@profoundlogic/coderflow-cli 0.12.144 → 0.13.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/README.md +29 -0
- package/coder.js +1 -1
- package/lib/commands/attach.js +61 -5
- package/lib/commands/containers.js +69 -16
- package/lib/commands/login.js +8 -0
- package/lib/commands/status.js +32 -3
- package/lib/config.js +4 -0
- package/lib/help.js +20 -9
- package/lib/http-client.js +46 -26
- package/lib/oidc.js +9 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -51,6 +51,15 @@ coder discard --yes
|
|
|
51
51
|
## Attaching to Containers
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
+
# Discover interactive containers and running containers retained by completed tasks
|
|
55
|
+
coder containers
|
|
56
|
+
|
|
57
|
+
# Include stopped, unknown-state, and active-task containers
|
|
58
|
+
coder containers --all
|
|
59
|
+
|
|
60
|
+
# Emit machine-readable output without a human-oriented preamble
|
|
61
|
+
coder containers --json
|
|
62
|
+
|
|
54
63
|
# Connect to last container
|
|
55
64
|
coder attach
|
|
56
65
|
|
|
@@ -59,8 +68,28 @@ coder attach --shell
|
|
|
59
68
|
|
|
60
69
|
# Connect to specific container
|
|
61
70
|
coder attach <container-id>
|
|
71
|
+
|
|
72
|
+
# Connect to a completed task's retained container without finding its container ID
|
|
73
|
+
coder attach <task-id>
|
|
74
|
+
|
|
75
|
+
# Show task state and retained-container state separately
|
|
76
|
+
coder status <task-id>
|
|
77
|
+
|
|
78
|
+
# Emit task and container status as JSON
|
|
79
|
+
coder status <task-id> --json
|
|
62
80
|
```
|
|
63
81
|
|
|
82
|
+
The default container list preserves existing interactive-container entries and
|
|
83
|
+
adds running retained-task containers. Retained entries show both task status
|
|
84
|
+
and container status plus an attach command using the public task ID.
|
|
85
|
+
Task-backed entries require both container-shell access and normal task
|
|
86
|
+
visibility; containers for private (`shared: false`) tasks are shown only to
|
|
87
|
+
owners who retain `tasks:view` or an administrator with `tasks:view_any`.
|
|
88
|
+
The `tasks:view_any` permission is independently sufficient for private-task
|
|
89
|
+
visibility.
|
|
90
|
+
When supplying a container ID directly, use the full ID or an unambiguous
|
|
91
|
+
prefix of at least 12 hexadecimal characters.
|
|
92
|
+
|
|
64
93
|
## Configuration and Profiles
|
|
65
94
|
|
|
66
95
|
Manage connection settings:
|
package/coder.js
CHANGED
package/lib/commands/attach.js
CHANGED
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Command: coder attach [container-id] - Attach to a running container
|
|
2
|
+
* Command: coder attach [container-or-task-id] - Attach to a running container
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { request } from '../http-client.js';
|
|
6
6
|
import { getLastContainerId, saveLastContainerId } from '../config.js';
|
|
7
7
|
import { connectTerminal } from '../terminal-client.js';
|
|
8
8
|
|
|
9
|
+
export function looksLikeTaskId(value) {
|
|
10
|
+
return /^\d{10,}-[a-z0-9]+$/i.test(value || '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatAttachLookupError(error) {
|
|
14
|
+
return error?.serverMessage
|
|
15
|
+
|| error?.responseBody?.message
|
|
16
|
+
|| error?.message
|
|
17
|
+
|| 'Unknown error';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve either a task ID or a container ID to attachable container details.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveAttachTarget(target, requestFn = request) {
|
|
24
|
+
if (!looksLikeTaskId(target)) {
|
|
25
|
+
return {
|
|
26
|
+
containerInfo: await requestFn(`/containers/${encodeURIComponent(target)}`, { exitOnError: false }),
|
|
27
|
+
taskId: null
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const task = await requestFn(`/tasks/${encodeURIComponent(target)}`, { exitOnError: false });
|
|
32
|
+
if (!task.containerId) {
|
|
33
|
+
throw new Error(`Task ${target} does not have a retained container`);
|
|
34
|
+
}
|
|
35
|
+
if (task.containerState === 'not_found') {
|
|
36
|
+
throw new Error(`Task ${target}'s retained container no longer exists`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let containerInfo;
|
|
40
|
+
try {
|
|
41
|
+
containerInfo = await requestFn(`/containers/${encodeURIComponent(task.containerId)}`, { exitOnError: false });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.statusCode === 404) {
|
|
44
|
+
throw new Error(`Task ${target}'s retained container no longer exists`, { cause: error });
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
containerInfo: {
|
|
50
|
+
...containerInfo,
|
|
51
|
+
isTaskContainer: true,
|
|
52
|
+
taskId: containerInfo.taskId || target,
|
|
53
|
+
taskStatus: containerInfo.taskStatus || task.status
|
|
54
|
+
},
|
|
55
|
+
taskId: target
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
9
59
|
export async function attachToContainer(args = []) {
|
|
10
60
|
let containerId = null;
|
|
11
61
|
let startShell = false;
|
|
@@ -38,7 +88,7 @@ export async function attachToContainer(args = []) {
|
|
|
38
88
|
if (!containerId) {
|
|
39
89
|
console.error('Error: No container ID specified and no previous container found.');
|
|
40
90
|
console.error('\nUsage:');
|
|
41
|
-
console.error(' coder attach <container-id>
|
|
91
|
+
console.error(' coder attach <container-or-task-id> # Attach to specific container or task');
|
|
42
92
|
console.error(' coder attach # Attach to last container');
|
|
43
93
|
console.error('\nTo see available containers, run:');
|
|
44
94
|
console.error(' coder containers');
|
|
@@ -46,15 +96,20 @@ export async function attachToContainer(args = []) {
|
|
|
46
96
|
}
|
|
47
97
|
console.log(`Attaching to last container: ${containerId}`);
|
|
48
98
|
} else {
|
|
49
|
-
|
|
99
|
+
const targetType = looksLikeTaskId(containerId) ? 'task' : 'container';
|
|
100
|
+
console.log(`Attaching to ${targetType}: ${containerId}`);
|
|
50
101
|
}
|
|
51
102
|
|
|
52
103
|
// Fetch container info from server to verify it exists and get details
|
|
53
104
|
let containerInfo;
|
|
54
105
|
try {
|
|
55
|
-
|
|
106
|
+
const resolved = await resolveAttachTarget(containerId);
|
|
107
|
+
containerInfo = resolved.containerInfo;
|
|
108
|
+
if (resolved.taskId) {
|
|
109
|
+
console.log(`Resolved task to container: ${containerInfo.containerId}`);
|
|
110
|
+
}
|
|
56
111
|
} catch (error) {
|
|
57
|
-
console.error(`\n✗ Failed to find container: ${error
|
|
112
|
+
console.error(`\n✗ Failed to find container: ${formatAttachLookupError(error)}`);
|
|
58
113
|
console.error('\nTo see available containers, run:');
|
|
59
114
|
console.error(' coder containers');
|
|
60
115
|
process.exit(1);
|
|
@@ -69,6 +124,7 @@ export async function attachToContainer(args = []) {
|
|
|
69
124
|
console.log(` Status: ${containerInfo.status}`);
|
|
70
125
|
if (containerInfo.isTaskContainer) {
|
|
71
126
|
console.log(` Type: Task container (${containerInfo.taskId})`);
|
|
127
|
+
console.log(` Task Status: ${containerInfo.taskStatus || 'unknown'}`);
|
|
72
128
|
}
|
|
73
129
|
|
|
74
130
|
if (containerInfo.status !== 'running') {
|
|
@@ -38,6 +38,24 @@ function parseCleanArgs(args) {
|
|
|
38
38
|
return options;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function parseListArgs(args) {
|
|
42
|
+
const options = { json: false, all: false };
|
|
43
|
+
|
|
44
|
+
for (const arg of args) {
|
|
45
|
+
if (arg === '--json') {
|
|
46
|
+
options.json = true;
|
|
47
|
+
} else if (arg === '--all' || arg === '-a') {
|
|
48
|
+
options.all = true;
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`Error: Unknown option: ${arg}`);
|
|
51
|
+
console.error('Usage: coder containers [--all] [--json]');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
async function confirm(message) {
|
|
42
60
|
const rl = createInterface({
|
|
43
61
|
input: process.stdin,
|
|
@@ -84,18 +102,38 @@ function filterContainers(containers, options) {
|
|
|
84
102
|
});
|
|
85
103
|
}
|
|
86
104
|
|
|
87
|
-
async function fetchContainers() {
|
|
88
|
-
const
|
|
105
|
+
async function fetchContainers({ includeTaskContainers = false } = {}, requestFn = request) {
|
|
106
|
+
const path = includeTaskContainers ? '/containers?include=tasks' : '/containers';
|
|
107
|
+
const data = await requestFn(path);
|
|
89
108
|
return data.containers || [];
|
|
90
109
|
}
|
|
91
110
|
|
|
92
|
-
async function listContainers() {
|
|
93
|
-
|
|
111
|
+
export async function listContainers(args = [], requestFn = request) {
|
|
112
|
+
const options = parseListArgs(args);
|
|
113
|
+
if (!options.json) {
|
|
114
|
+
console.log('Fetching Coder containers...\n');
|
|
115
|
+
}
|
|
94
116
|
|
|
95
|
-
const
|
|
117
|
+
const fetchedContainers = await fetchContainers({ includeTaskContainers: true }, requestFn);
|
|
118
|
+
// Interactive containers preserve the legacy list behavior. The default task
|
|
119
|
+
// view focuses on the newly discoverable/attachable case from this guide:
|
|
120
|
+
// retained containers that are still running. --all exposes the rest.
|
|
121
|
+
const containers = options.all
|
|
122
|
+
? fetchedContainers
|
|
123
|
+
: fetchedContainers.filter(container => (
|
|
124
|
+
!container.isTaskContainer || (container.retained && container.status === 'running')
|
|
125
|
+
));
|
|
126
|
+
|
|
127
|
+
if (options.json) {
|
|
128
|
+
console.log(JSON.stringify({ count: containers.length, containers }, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
96
131
|
|
|
97
132
|
if (containers.length === 0) {
|
|
98
133
|
console.log('No Coder containers found.');
|
|
134
|
+
if (!options.all) {
|
|
135
|
+
console.log('Use "coder containers --all" to include stopped, unknown-state, and active-task containers.');
|
|
136
|
+
}
|
|
99
137
|
return;
|
|
100
138
|
}
|
|
101
139
|
|
|
@@ -105,6 +143,17 @@ async function listContainers() {
|
|
|
105
143
|
const statusIcon = container.status === 'running' ? '▶' : '■';
|
|
106
144
|
console.log(` ${statusIcon} ${container.name} (${container.containerId})`);
|
|
107
145
|
console.log(` Status: ${container.status}`);
|
|
146
|
+
if (container.isTaskContainer) {
|
|
147
|
+
console.log(` Type: ${container.retained ? 'Retained task container' : 'Task container'}`);
|
|
148
|
+
console.log(` Task: ${container.taskId}`);
|
|
149
|
+
console.log(` Task status: ${container.taskStatus || 'unknown'}`);
|
|
150
|
+
if (container.taskName) {
|
|
151
|
+
console.log(` Task name: ${container.taskName}`);
|
|
152
|
+
}
|
|
153
|
+
if (container.status === 'running') {
|
|
154
|
+
console.log(` Attach: coder attach ${container.taskId}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
108
157
|
if (container.environment) {
|
|
109
158
|
console.log(` Env: ${container.environment}`);
|
|
110
159
|
}
|
|
@@ -115,18 +164,20 @@ async function listContainers() {
|
|
|
115
164
|
}
|
|
116
165
|
}
|
|
117
166
|
|
|
118
|
-
async function deleteContainer(container) {
|
|
119
|
-
await
|
|
167
|
+
async function deleteContainer(container, requestFn = request) {
|
|
168
|
+
await requestFn(`/containers/${container.name}`, {
|
|
120
169
|
method: 'DELETE'
|
|
121
170
|
});
|
|
122
171
|
}
|
|
123
172
|
|
|
124
|
-
async function cleanContainers(args) {
|
|
173
|
+
export async function cleanContainers(args, requestFn = request) {
|
|
125
174
|
const options = parseCleanArgs(args);
|
|
126
175
|
|
|
127
176
|
console.log('Fetching Coder containers...\n');
|
|
128
177
|
|
|
129
|
-
|
|
178
|
+
// Task containers have their own lifecycle and must never be candidates for
|
|
179
|
+
// the interactive-container cleanup command.
|
|
180
|
+
const allContainers = await fetchContainers({}, requestFn);
|
|
130
181
|
|
|
131
182
|
if (allContainers.length === 0) {
|
|
132
183
|
console.log('No Coder containers found. Nothing to clean.');
|
|
@@ -181,7 +232,7 @@ async function cleanContainers(args) {
|
|
|
181
232
|
|
|
182
233
|
for (const container of containersToDelete) {
|
|
183
234
|
try {
|
|
184
|
-
await deleteContainer(container);
|
|
235
|
+
await deleteContainer(container, requestFn);
|
|
185
236
|
deletedCount += 1;
|
|
186
237
|
} catch (error) {
|
|
187
238
|
console.error(`✗ Failed to delete ${container.name}: ${error.message}`);
|
|
@@ -196,17 +247,19 @@ async function cleanContainers(args) {
|
|
|
196
247
|
}
|
|
197
248
|
}
|
|
198
249
|
|
|
199
|
-
export async function handleContainers(args = []) {
|
|
250
|
+
export async function handleContainers(args = [], requestFn = request) {
|
|
200
251
|
const subcommand = args[0];
|
|
201
252
|
|
|
202
253
|
if (subcommand === 'clean') {
|
|
203
|
-
await cleanContainers(args.slice(1));
|
|
204
|
-
} else if (!subcommand) {
|
|
205
|
-
await listContainers();
|
|
254
|
+
await cleanContainers(args.slice(1), requestFn);
|
|
255
|
+
} else if (!subcommand || subcommand.startsWith('-')) {
|
|
256
|
+
await listContainers(args, requestFn);
|
|
206
257
|
} else {
|
|
207
258
|
console.error(`Error: Unknown subcommand: ${subcommand}`);
|
|
208
|
-
console.error('Usage: coder containers [clean]');
|
|
209
|
-
console.error(' coder containers # List
|
|
259
|
+
console.error('Usage: coder containers [--all] [--json|clean]');
|
|
260
|
+
console.error(' coder containers # List interactive and task containers');
|
|
261
|
+
console.error(' coder containers --all # Include stopped, unknown, and active-task containers');
|
|
262
|
+
console.error(' coder containers --json # List containers as JSON');
|
|
210
263
|
console.error(' coder containers clean # Clean containers');
|
|
211
264
|
process.exit(1);
|
|
212
265
|
}
|
package/lib/commands/login.js
CHANGED
|
@@ -235,6 +235,14 @@ async function ssoLogin() {
|
|
|
235
235
|
console.error(`Error: Lost connection to server: ${error.message}`);
|
|
236
236
|
process.exit(1);
|
|
237
237
|
}
|
|
238
|
+
if (error.code === 'INVALID_SSO_APPROVAL_RESPONSE') {
|
|
239
|
+
process.removeListener('SIGINT', cleanup);
|
|
240
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
241
|
+
console.error('');
|
|
242
|
+
console.error(`Error: ${error.message}.`);
|
|
243
|
+
console.error('Please update the CLI/server or contact your administrator.');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
238
246
|
// For other errors, continue polling
|
|
239
247
|
}
|
|
240
248
|
|
package/lib/commands/status.js
CHANGED
|
@@ -4,16 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
import { request } from '../http-client.js';
|
|
6
6
|
|
|
7
|
-
export async function getStatus(taskId) {
|
|
7
|
+
export async function getStatus(taskId, args = [], requestFn = request) {
|
|
8
8
|
if (!taskId) {
|
|
9
9
|
console.error('Error: Task ID required');
|
|
10
10
|
console.error('Usage: coder status <task-id>');
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const json = args.includes('--json');
|
|
15
|
+
const unknownOption = args.find(arg => arg !== '--json');
|
|
16
|
+
if (unknownOption) {
|
|
17
|
+
console.error(`Error: Unknown option: ${unknownOption}`);
|
|
18
|
+
console.error('Usage: coder status <task-id> [--json]');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
if (!json) {
|
|
23
|
+
console.log(`Fetching status for task ${taskId}...`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = await requestFn(`/tasks/${taskId}`);
|
|
27
|
+
|
|
28
|
+
if (json) {
|
|
29
|
+
console.log(JSON.stringify(data, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
17
32
|
|
|
18
33
|
console.log(`\nTask Status:`);
|
|
19
34
|
console.log(` Task ID: ${data.taskId}`);
|
|
@@ -28,6 +43,20 @@ export async function getStatus(taskId) {
|
|
|
28
43
|
console.log(` Exit Code: ${data.exitCode}`);
|
|
29
44
|
}
|
|
30
45
|
|
|
46
|
+
if (data.containerId) {
|
|
47
|
+
const containerState = data.containerState || 'unknown';
|
|
48
|
+
const containerStateLabel = containerState === 'not_found'
|
|
49
|
+
? 'not found'
|
|
50
|
+
: (containerState === 'exited' || containerState === 'dead' ? 'stopped' : containerState);
|
|
51
|
+
console.log(` Container ID: ${data.containerId.substring(0, 12)}`);
|
|
52
|
+
console.log(` Container: ${containerStateLabel}`);
|
|
53
|
+
if (containerState === 'running') {
|
|
54
|
+
console.log(` Attach: coder attach ${taskId}`);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
console.log(' Container: not available');
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
// Custom task detail fields (per-environment definitions; values on the task)
|
|
32
61
|
const customFields = data.customFields || {};
|
|
33
62
|
if (Object.keys(customFields).length > 0) {
|
package/lib/config.js
CHANGED
|
@@ -165,6 +165,10 @@ export async function getCredentialsPathForDisplay() {
|
|
|
165
165
|
* Save API key to config file or active profile
|
|
166
166
|
*/
|
|
167
167
|
export async function saveApiKey(apiKey) {
|
|
168
|
+
if (typeof apiKey !== 'string' || apiKey.trim().length === 0) {
|
|
169
|
+
throw new Error('Cannot save an empty API key');
|
|
170
|
+
}
|
|
171
|
+
|
|
168
172
|
// Check if there's an active profile (including CLI override)
|
|
169
173
|
const activeProfileName = cliProfileOverride || await getActiveProfileName();
|
|
170
174
|
|
package/lib/help.js
CHANGED
|
@@ -38,12 +38,12 @@ Examples:
|
|
|
38
38
|
`,
|
|
39
39
|
|
|
40
40
|
attach: `
|
|
41
|
-
Usage: coder attach [container-id] [options]
|
|
41
|
+
Usage: coder attach [container-or-task-id] [options]
|
|
42
42
|
|
|
43
43
|
Connect to a running container.
|
|
44
44
|
|
|
45
45
|
Arguments:
|
|
46
|
-
container-id
|
|
46
|
+
container-or-task-id Container or task ID to attach to (optional - uses last container if omitted)
|
|
47
47
|
|
|
48
48
|
Options:
|
|
49
49
|
--agent=<agent> Agent to use (claude, codex, gemini, bob, grok, kimi)
|
|
@@ -52,6 +52,7 @@ Options:
|
|
|
52
52
|
Examples:
|
|
53
53
|
coder attach # Reconnect to last container
|
|
54
54
|
coder attach 188b8e72d656 # Connect to specific container
|
|
55
|
+
coder attach 1784748839353-e7hk4lh2c # Connect to a task's retained container
|
|
55
56
|
coder attach --shell # Connect with bash shell
|
|
56
57
|
coder attach --agent=codex # Connect with specific agent
|
|
57
58
|
`,
|
|
@@ -139,13 +140,16 @@ Examples:
|
|
|
139
140
|
`,
|
|
140
141
|
|
|
141
142
|
status: `
|
|
142
|
-
Usage: coder status <task-id>
|
|
143
|
+
Usage: coder status <task-id> [--json]
|
|
143
144
|
|
|
144
|
-
Check
|
|
145
|
+
Check task status and retained-container availability.
|
|
145
146
|
|
|
146
147
|
Arguments:
|
|
147
148
|
task-id Task ID to check (required)
|
|
148
149
|
|
|
150
|
+
Options:
|
|
151
|
+
--json Print machine-readable JSON only
|
|
152
|
+
|
|
149
153
|
Examples:
|
|
150
154
|
coder status 1759542727986-3uoyf48lr
|
|
151
155
|
`,
|
|
@@ -275,11 +279,16 @@ Examples:
|
|
|
275
279
|
containers: `
|
|
276
280
|
Usage: coder containers [command] [options]
|
|
277
281
|
|
|
278
|
-
List or clean containers.
|
|
282
|
+
List or clean containers. The default list includes interactive containers and
|
|
283
|
+
running task containers retained after completion.
|
|
279
284
|
|
|
280
285
|
Commands:
|
|
281
|
-
(none) List
|
|
282
|
-
clean Clean up containers
|
|
286
|
+
(none) List interactive and task containers
|
|
287
|
+
clean Clean up interactive containers
|
|
288
|
+
|
|
289
|
+
Options (for list):
|
|
290
|
+
--all, -a Include stopped, unknown-state, and active-task containers
|
|
291
|
+
--json Print machine-readable JSON only
|
|
283
292
|
|
|
284
293
|
Options (for clean):
|
|
285
294
|
--stopped Clean stopped containers
|
|
@@ -289,6 +298,8 @@ Options (for clean):
|
|
|
289
298
|
|
|
290
299
|
Examples:
|
|
291
300
|
coder containers
|
|
301
|
+
coder containers --all
|
|
302
|
+
coder containers --json
|
|
292
303
|
coder containers clean --stopped --yes
|
|
293
304
|
coder containers clean --older-than=7d --dry-run
|
|
294
305
|
`
|
|
@@ -315,7 +326,7 @@ Usage: coder <command> [options]
|
|
|
315
326
|
Commands:
|
|
316
327
|
coder apply [task-id] Apply patches from completed task to local repos
|
|
317
328
|
coder discard [--env=<environment>] [--yes] Discard applied changes from repos
|
|
318
|
-
coder attach [container-id] [options]
|
|
329
|
+
coder attach [container-or-task-id] [options] Connect to a running container
|
|
319
330
|
|
|
320
331
|
Setup:
|
|
321
332
|
coder login [--sso] Authenticate with server
|
|
@@ -355,7 +366,7 @@ Applying Changes:
|
|
|
355
366
|
coder discard [--env=<environment>] [--yes] Discard applied changes from repos
|
|
356
367
|
|
|
357
368
|
Connecting to Containers:
|
|
358
|
-
coder attach [container-id] [--agent=<agent>] [--shell] Connect to a running container
|
|
369
|
+
coder attach [container-or-task-id] [--agent=<agent>] [--shell] Connect to a running container
|
|
359
370
|
|
|
360
371
|
Setup & Configuration:
|
|
361
372
|
coder login [--sso] Authenticate with server
|
package/lib/http-client.js
CHANGED
|
@@ -4,6 +4,48 @@
|
|
|
4
4
|
|
|
5
5
|
import { getServerUrl, getApiKey } from './config.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Build a consistent error object from an HTTP error response.
|
|
9
|
+
*/
|
|
10
|
+
export function buildHttpError(status, data) {
|
|
11
|
+
let message;
|
|
12
|
+
let detailedError = null;
|
|
13
|
+
|
|
14
|
+
if (typeof data === 'string') {
|
|
15
|
+
message = data.trim() || `HTTP ${status}`;
|
|
16
|
+
detailedError = data.trim();
|
|
17
|
+
} else {
|
|
18
|
+
// A 400 response commonly uses `error` as a generic label ("Bad Request")
|
|
19
|
+
// and `message` for the actionable reason. Surface the useful reason first.
|
|
20
|
+
message = (status === 400 && data?.message)
|
|
21
|
+
|| data?.error
|
|
22
|
+
|| data?.message
|
|
23
|
+
|| `HTTP ${status}`;
|
|
24
|
+
|
|
25
|
+
if (data?.error && data?.message) {
|
|
26
|
+
detailedError = `${data.error}: ${data.message}`;
|
|
27
|
+
} else if (data?.message) {
|
|
28
|
+
detailedError = data.message;
|
|
29
|
+
} else if (data?.error) {
|
|
30
|
+
detailedError = data.error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const error = new Error(message);
|
|
35
|
+
error.detailedError = detailedError;
|
|
36
|
+
error.statusCode = status;
|
|
37
|
+
if (data && typeof data === 'object') {
|
|
38
|
+
error.responseBody = data;
|
|
39
|
+
error.serverMessage = typeof data.message === 'string' ? data.message : null;
|
|
40
|
+
for (const field of ['errorCode', 'requiredPermission', 'containerId', 'taskId', 'details']) {
|
|
41
|
+
if (Object.prototype.hasOwnProperty.call(data, field)) {
|
|
42
|
+
error[field] = data[field];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return error;
|
|
47
|
+
}
|
|
48
|
+
|
|
7
49
|
/**
|
|
8
50
|
* Format a helpful error message based on error type
|
|
9
51
|
*/
|
|
@@ -106,10 +148,9 @@ function formatErrorMessage(error, url, response, serverUrl) {
|
|
|
106
148
|
export async function request(path, options = {}) {
|
|
107
149
|
const serverUrl = await getServerUrl();
|
|
108
150
|
const url = `${serverUrl}${path}`;
|
|
151
|
+
const { parse = 'json', exitOnError = true, ...fetchOptions } = options;
|
|
109
152
|
|
|
110
153
|
try {
|
|
111
|
-
const { parse = 'json', ...fetchOptions } = options;
|
|
112
|
-
|
|
113
154
|
// Get API key and add to headers if available
|
|
114
155
|
const apiKey = await getApiKey();
|
|
115
156
|
const headers = {
|
|
@@ -140,30 +181,9 @@ export async function request(path, options = {}) {
|
|
|
140
181
|
}
|
|
141
182
|
|
|
142
183
|
if (!response.ok) {
|
|
143
|
-
|
|
144
|
-
let detailedError = null;
|
|
145
|
-
|
|
146
|
-
if (typeof data === 'string') {
|
|
147
|
-
message = data.trim() || `HTTP ${response.status}`;
|
|
148
|
-
// Try to parse error details from string response
|
|
149
|
-
detailedError = data.trim();
|
|
150
|
-
} else {
|
|
151
|
-
message = data?.error || data?.message || `HTTP ${response.status}`;
|
|
152
|
-
// Capture additional error details if available
|
|
153
|
-
if (data?.error && data?.message) {
|
|
154
|
-
detailedError = `${data.error}: ${data.message}`;
|
|
155
|
-
} else if (data?.message) {
|
|
156
|
-
detailedError = data.message;
|
|
157
|
-
} else if (data?.error) {
|
|
158
|
-
detailedError = data.error;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const error = new Error(message);
|
|
163
|
-
error.detailedError = detailedError;
|
|
164
|
-
error.statusCode = response.status;
|
|
184
|
+
const error = buildHttpError(response.status, data);
|
|
165
185
|
|
|
166
|
-
if (formatErrorMessage(error, url, response, serverUrl)) {
|
|
186
|
+
if (exitOnError && formatErrorMessage(error, url, response, serverUrl)) {
|
|
167
187
|
process.exit(1);
|
|
168
188
|
}
|
|
169
189
|
throw error;
|
|
@@ -172,7 +192,7 @@ export async function request(path, options = {}) {
|
|
|
172
192
|
return data;
|
|
173
193
|
} catch (error) {
|
|
174
194
|
// Handle network/connection errors
|
|
175
|
-
if (formatErrorMessage(error, url, null, serverUrl)) {
|
|
195
|
+
if (!error.statusCode && exitOnError && formatErrorMessage(error, url, null, serverUrl)) {
|
|
176
196
|
process.exit(1);
|
|
177
197
|
}
|
|
178
198
|
throw error;
|
package/lib/oidc.js
CHANGED
|
@@ -111,11 +111,18 @@ export async function pollDeviceFlow(deviceCode) {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const data = await response.json();
|
|
114
|
+
const apiKey = data.api_key || data.apiKey;
|
|
115
|
+
|
|
116
|
+
if (data.status === 'approved' || apiKey) {
|
|
117
|
+
if (!apiKey) {
|
|
118
|
+
const error = new Error('SSO approval response did not include an API key');
|
|
119
|
+
error.code = 'INVALID_SSO_APPROVAL_RESPONSE';
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
114
122
|
|
|
115
|
-
if (data.status === 'approved' || data.apiKey) {
|
|
116
123
|
return {
|
|
117
124
|
status: 'approved',
|
|
118
|
-
apiKey
|
|
125
|
+
apiKey,
|
|
119
126
|
user: data.user
|
|
120
127
|
};
|
|
121
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profoundlogic/coderflow-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "AI Coder CLI - Command-line interface for managing AI coding tasks",
|
|
5
5
|
"main": "coder.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test:all": "node --test tests/**/*.test.js"
|
|
13
13
|
},
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=24"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"coder.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
29
29
|
"homepage": "https://coderflow.ai",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"ws": "^8.
|
|
32
|
-
"@inquirer/prompts": "^
|
|
33
|
-
"open": "^
|
|
31
|
+
"ws": "^8.21.1",
|
|
32
|
+
"@inquirer/prompts": "^8.5.2",
|
|
33
|
+
"open": "^11.0.0"
|
|
34
34
|
}
|
|
35
35
|
}
|