@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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder list - List all tasks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request } from '../http-client.js';
|
|
6
|
+
|
|
7
|
+
function parseListArgs(args = []) {
|
|
8
|
+
const options = {
|
|
9
|
+
status: null,
|
|
10
|
+
environment: null
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
for (const arg of args) {
|
|
14
|
+
if (arg.startsWith('--status=')) {
|
|
15
|
+
options.status = arg.substring('--status='.length);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (arg.startsWith('--environment=')) {
|
|
20
|
+
options.environment = arg.substring('--environment='.length);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.error(`Error: Unknown option ${arg}`);
|
|
25
|
+
console.error('Usage: coder list [--status=running|completed|failed|rejected] [--environment=name]');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatDuration(created, finished) {
|
|
33
|
+
if (!created) {
|
|
34
|
+
return 'unknown';
|
|
35
|
+
}
|
|
36
|
+
const start = new Date(created).getTime();
|
|
37
|
+
const end = finished ? new Date(finished).getTime() : Date.now();
|
|
38
|
+
const diff = Math.max(0, end - start);
|
|
39
|
+
const minutes = Math.floor(diff / (1000 * 60));
|
|
40
|
+
const seconds = Math.floor((diff / 1000) % 60);
|
|
41
|
+
if (minutes > 0) {
|
|
42
|
+
return `${minutes}m ${seconds}s`;
|
|
43
|
+
}
|
|
44
|
+
return `${seconds}s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function listTasks(args = []) {
|
|
48
|
+
const filters = parseListArgs(args);
|
|
49
|
+
|
|
50
|
+
console.log('Fetching tasks...');
|
|
51
|
+
|
|
52
|
+
const query = new URLSearchParams();
|
|
53
|
+
if (filters.status) {
|
|
54
|
+
query.append('status', filters.status);
|
|
55
|
+
}
|
|
56
|
+
if (filters.environment) {
|
|
57
|
+
query.append('environment', filters.environment);
|
|
58
|
+
}
|
|
59
|
+
query.append('include', 'stats');
|
|
60
|
+
|
|
61
|
+
const path = query.toString() ? `/tasks?${query.toString()}` : '/tasks';
|
|
62
|
+
const data = await request(path);
|
|
63
|
+
|
|
64
|
+
console.log(`\nTasks (${data.count} total)`);
|
|
65
|
+
if (filters.status) {
|
|
66
|
+
console.log(` Status: ${filters.status}`);
|
|
67
|
+
}
|
|
68
|
+
if (filters.environment) {
|
|
69
|
+
console.log(` Environment: ${filters.environment}`);
|
|
70
|
+
}
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
if (data.tasks.length === 0) {
|
|
74
|
+
console.log(' No tasks found');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const task of data.tasks) {
|
|
79
|
+
const statusIcon = task.status === 'completed' ? '✓'
|
|
80
|
+
: task.status === 'failed' ? '✗'
|
|
81
|
+
: task.status === 'rejected' ? '!' : '⋯';
|
|
82
|
+
|
|
83
|
+
console.log(` ${statusIcon} ${task.taskId}`);
|
|
84
|
+
console.log(` Status: ${task.status}`);
|
|
85
|
+
if (task.environment) {
|
|
86
|
+
console.log(` Environment: ${task.environment}`);
|
|
87
|
+
}
|
|
88
|
+
if (task.taskType) {
|
|
89
|
+
console.log(` Task Type: ${task.taskType}`);
|
|
90
|
+
}
|
|
91
|
+
console.log(` Created: ${task.createdAt || 'unknown'}`);
|
|
92
|
+
if (task.finishedAt) {
|
|
93
|
+
console.log(` Finished: ${task.finishedAt}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(` Duration: ${formatDuration(task.createdAt, task.finishedAt)}`);
|
|
96
|
+
if (task.stats) {
|
|
97
|
+
const filesChanged = Number(task.stats.filesChanged) || 0;
|
|
98
|
+
const linesAdded = Number(task.stats.linesAdded) || 0;
|
|
99
|
+
const linesDeleted = Number(task.stats.linesDeleted) || 0;
|
|
100
|
+
console.log(` Changes: ${filesChanged} files, +${linesAdded} -${linesDeleted}`);
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder login - Authenticate with the Coder server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { request } from '../http-client.js';
|
|
7
|
+
import { saveApiKey, getCredentialsPathForDisplay } from '../config.js';
|
|
8
|
+
import { getActiveProfileName } from '../profile.js';
|
|
9
|
+
import { checkOidcConfig, initiateDeviceFlow, pollDeviceFlow } from '../oidc.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Prompt for input from stdin
|
|
13
|
+
*/
|
|
14
|
+
function prompt(question) {
|
|
15
|
+
const rl = readline.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(question, (answer) => {
|
|
22
|
+
rl.close();
|
|
23
|
+
resolve(answer);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prompt for password (hidden input)
|
|
30
|
+
*/
|
|
31
|
+
function promptPassword(question) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Hide password input
|
|
39
|
+
const stdin = process.stdin;
|
|
40
|
+
const onData = () => {};
|
|
41
|
+
|
|
42
|
+
stdin.on('data', onData);
|
|
43
|
+
|
|
44
|
+
rl.question(question, (password) => {
|
|
45
|
+
stdin.removeListener('data', onData);
|
|
46
|
+
rl.close();
|
|
47
|
+
console.log(''); // New line after password
|
|
48
|
+
resolve(password);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Disable echo
|
|
52
|
+
rl._writeToOutput = function(stringToWrite) {
|
|
53
|
+
if (stringToWrite.charCodeAt(0) === 13) {
|
|
54
|
+
rl.output.write('\n');
|
|
55
|
+
} else if (rl.line.length === 0) {
|
|
56
|
+
rl.output.write(question);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Draw a box around text for visual emphasis
|
|
64
|
+
*/
|
|
65
|
+
function drawBox(content, title = '') {
|
|
66
|
+
const lines = content.split('\n');
|
|
67
|
+
const maxWidth = Math.max(...lines.map(l => l.length), title.length);
|
|
68
|
+
const width = maxWidth + 4;
|
|
69
|
+
|
|
70
|
+
const horizontal = '─'.repeat(width);
|
|
71
|
+
const top = title
|
|
72
|
+
? `┌─ ${title} ${'─'.repeat(width - title.length - 3)}┐`
|
|
73
|
+
: `┌${horizontal}┐`;
|
|
74
|
+
|
|
75
|
+
console.log(top);
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const padding = ' '.repeat(maxWidth - line.length);
|
|
78
|
+
console.log(`│ ${line}${padding} │`);
|
|
79
|
+
}
|
|
80
|
+
console.log(`└${horizontal}┘`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* SSO login flow using OIDC device flow
|
|
85
|
+
*/
|
|
86
|
+
async function ssoLogin() {
|
|
87
|
+
console.log('SSO Login');
|
|
88
|
+
console.log('');
|
|
89
|
+
|
|
90
|
+
// Check if OIDC is enabled
|
|
91
|
+
console.log('Checking SSO configuration...');
|
|
92
|
+
let oidcConfig;
|
|
93
|
+
try {
|
|
94
|
+
oidcConfig = await checkOidcConfig();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('');
|
|
97
|
+
console.error(`Error: Failed to check SSO configuration: ${error.message}`);
|
|
98
|
+
console.error('');
|
|
99
|
+
console.error('Make sure the server is running and accessible.');
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!oidcConfig.enabled) {
|
|
104
|
+
console.error('');
|
|
105
|
+
console.error('Error: SSO is not enabled on this server.');
|
|
106
|
+
console.error('');
|
|
107
|
+
console.error('Please contact your administrator to enable OIDC authentication,');
|
|
108
|
+
console.error('or use `coder login` without --sso for username/password login.');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Initiate device flow
|
|
113
|
+
console.log('Initiating SSO authentication...');
|
|
114
|
+
let deviceFlow;
|
|
115
|
+
try {
|
|
116
|
+
deviceFlow = await initiateDeviceFlow();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('');
|
|
119
|
+
console.error(`Error: Failed to start SSO flow: ${error.message}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log('');
|
|
124
|
+
|
|
125
|
+
// Display user code prominently
|
|
126
|
+
drawBox(deviceFlow.userCode, 'Your Code');
|
|
127
|
+
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('To sign in:');
|
|
130
|
+
console.log(` 1. Open: ${deviceFlow.verificationUrl}`);
|
|
131
|
+
console.log(' 2. Enter the code shown above');
|
|
132
|
+
console.log(' 3. Follow the prompts to authenticate');
|
|
133
|
+
console.log('');
|
|
134
|
+
|
|
135
|
+
// Try to open browser
|
|
136
|
+
try {
|
|
137
|
+
const open = (await import('open')).default;
|
|
138
|
+
await open(deviceFlow.verificationUrl);
|
|
139
|
+
console.log('Browser opened automatically.');
|
|
140
|
+
} catch {
|
|
141
|
+
console.log('Could not open browser automatically.');
|
|
142
|
+
console.log(`Please open this URL manually: ${deviceFlow.verificationUrl}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log('Waiting for authentication...');
|
|
147
|
+
|
|
148
|
+
// Set up Ctrl+C handler
|
|
149
|
+
let cancelled = false;
|
|
150
|
+
const cleanup = () => {
|
|
151
|
+
cancelled = true;
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log('Authentication cancelled.');
|
|
154
|
+
process.exit(0);
|
|
155
|
+
};
|
|
156
|
+
process.on('SIGINT', cleanup);
|
|
157
|
+
|
|
158
|
+
// Poll for approval
|
|
159
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
160
|
+
let spinnerIndex = 0;
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
const expiresAt = startTime + (deviceFlow.expiresIn * 1000);
|
|
163
|
+
const pollInterval = Math.max(deviceFlow.interval * 1000, 2000); // At least 2 seconds
|
|
164
|
+
|
|
165
|
+
while (!cancelled) {
|
|
166
|
+
// Check if expired
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
if (now >= expiresAt) {
|
|
169
|
+
process.removeListener('SIGINT', cleanup);
|
|
170
|
+
console.log('');
|
|
171
|
+
console.error('');
|
|
172
|
+
console.error('Error: Authentication session expired.');
|
|
173
|
+
console.error('Please run `coder login --sso` again to start a new session.');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Show spinner with time remaining
|
|
178
|
+
const remaining = Math.ceil((expiresAt - now) / 1000);
|
|
179
|
+
const minutes = Math.floor(remaining / 60);
|
|
180
|
+
const seconds = remaining % 60;
|
|
181
|
+
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
182
|
+
|
|
183
|
+
process.stdout.write(`\r${spinnerFrames[spinnerIndex]} Waiting for approval (expires in ${timeStr})... `);
|
|
184
|
+
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
185
|
+
|
|
186
|
+
// Poll
|
|
187
|
+
try {
|
|
188
|
+
const result = await pollDeviceFlow(deviceFlow.deviceCode);
|
|
189
|
+
|
|
190
|
+
if (result.status === 'approved') {
|
|
191
|
+
process.removeListener('SIGINT', cleanup);
|
|
192
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r'); // Clear spinner line
|
|
193
|
+
|
|
194
|
+
// Save API key
|
|
195
|
+
await saveApiKey(result.apiKey);
|
|
196
|
+
|
|
197
|
+
// Get the active profile name to show in output
|
|
198
|
+
const activeProfile = await getActiveProfileName();
|
|
199
|
+
const credentialsPath = await getCredentialsPathForDisplay();
|
|
200
|
+
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(`✓ Logged in as ${result.user?.name || result.user?.username || 'user'}`);
|
|
203
|
+
if (result.user?.email) {
|
|
204
|
+
console.log(` Email: ${result.user.email}`);
|
|
205
|
+
}
|
|
206
|
+
if (result.user?.role) {
|
|
207
|
+
console.log(` Role: ${result.user.role}`);
|
|
208
|
+
}
|
|
209
|
+
console.log('');
|
|
210
|
+
if (activeProfile) {
|
|
211
|
+
console.log(`Credentials saved to profile '${activeProfile}'`);
|
|
212
|
+
console.log(` Location: ${credentialsPath}`);
|
|
213
|
+
} else {
|
|
214
|
+
console.log(`Credentials saved to ${credentialsPath}`);
|
|
215
|
+
}
|
|
216
|
+
console.log('');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.status === 'expired') {
|
|
221
|
+
process.removeListener('SIGINT', cleanup);
|
|
222
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
223
|
+
console.error('');
|
|
224
|
+
console.error('Error: Authentication session expired.');
|
|
225
|
+
console.error('Please run `coder login --sso` again to start a new session.');
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (result.status === 'denied') {
|
|
230
|
+
process.removeListener('SIGINT', cleanup);
|
|
231
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
232
|
+
console.error('');
|
|
233
|
+
console.error('Error: Authentication was denied.');
|
|
234
|
+
console.error('Please try again or contact your administrator.');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Still pending, wait before polling again
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// Log error but keep polling unless it's fatal
|
|
241
|
+
if (error.message.includes('ECONNREFUSED') || error.message.includes('ENOTFOUND')) {
|
|
242
|
+
process.removeListener('SIGINT', cleanup);
|
|
243
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
244
|
+
console.error('');
|
|
245
|
+
console.error(`Error: Lost connection to server: ${error.message}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
// For other errors, continue polling
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Wait before next poll
|
|
252
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Username/password login flow
|
|
258
|
+
*/
|
|
259
|
+
async function passwordLogin() {
|
|
260
|
+
console.log('Coder Login');
|
|
261
|
+
console.log('');
|
|
262
|
+
|
|
263
|
+
// Prompt for username
|
|
264
|
+
const username = await prompt('Username: ');
|
|
265
|
+
|
|
266
|
+
if (!username || !username.trim()) {
|
|
267
|
+
console.error('Error: Username is required');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Prompt for password
|
|
272
|
+
const password = await promptPassword('Password: ');
|
|
273
|
+
|
|
274
|
+
if (!password) {
|
|
275
|
+
console.error('Error: Password is required');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log('Authenticating...');
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
// Call the CLI login endpoint (doesn't require auth)
|
|
283
|
+
const response = await request('/auth/cli-login', {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
username: username.trim(),
|
|
287
|
+
password
|
|
288
|
+
})
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Save API key to config file or active profile
|
|
292
|
+
await saveApiKey(response.apiKey);
|
|
293
|
+
|
|
294
|
+
// Get the active profile name to show in output
|
|
295
|
+
const activeProfile = await getActiveProfileName();
|
|
296
|
+
const credentialsPath = await getCredentialsPathForDisplay();
|
|
297
|
+
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log(`✓ Logged in as ${response.user.name || response.user.username}`);
|
|
300
|
+
console.log(` Email: ${response.user.email}`);
|
|
301
|
+
console.log(` Role: ${response.user.role}`);
|
|
302
|
+
console.log('');
|
|
303
|
+
if (activeProfile) {
|
|
304
|
+
console.log(`Credentials saved to profile '${activeProfile}'`);
|
|
305
|
+
console.log(` Location: ${credentialsPath}`);
|
|
306
|
+
} else {
|
|
307
|
+
console.log(`Credentials saved to ${credentialsPath}`);
|
|
308
|
+
}
|
|
309
|
+
console.log('');
|
|
310
|
+
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('');
|
|
313
|
+
console.error(`✗ Login failed: ${error.message}`);
|
|
314
|
+
console.error('');
|
|
315
|
+
console.error('Please check your username and password and try again.');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function login(args = []) {
|
|
321
|
+
// Parse --sso flag
|
|
322
|
+
const useSso = args.includes('--sso');
|
|
323
|
+
|
|
324
|
+
if (useSso) {
|
|
325
|
+
await ssoLogin();
|
|
326
|
+
} else {
|
|
327
|
+
await passwordLogin();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder logs - Show logs for a task
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request } from '../http-client.js';
|
|
6
|
+
|
|
7
|
+
function parseLogsArgs(args) {
|
|
8
|
+
const options = {
|
|
9
|
+
taskId: null,
|
|
10
|
+
tail: null
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
for (const arg of args) {
|
|
14
|
+
if (!options.taskId && !arg.startsWith('--')) {
|
|
15
|
+
options.taskId = arg;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (arg.startsWith('--tail=')) {
|
|
20
|
+
const value = arg.substring('--tail='.length);
|
|
21
|
+
const parsed = parseInt(value, 10);
|
|
22
|
+
if (Number.isNaN(parsed)) {
|
|
23
|
+
console.error('Error: --tail must be a number');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
options.tail = parsed;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.error(`Error: Unknown option: ${arg}`);
|
|
31
|
+
console.error('Usage: coder logs <task-id> [--tail=N]');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return options;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function showLogs(args = []) {
|
|
39
|
+
const { taskId, tail } = parseLogsArgs(args);
|
|
40
|
+
|
|
41
|
+
if (!taskId) {
|
|
42
|
+
console.error('Error: Task ID required');
|
|
43
|
+
console.error('Usage: coder logs <task-id> [--tail=N]');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`Fetching logs for task ${taskId}...`);
|
|
48
|
+
|
|
49
|
+
const query = tail ? `?tail=${tail}` : '';
|
|
50
|
+
const logs = await request(`/tasks/${taskId}/logs${query}`, {
|
|
51
|
+
parse: 'text',
|
|
52
|
+
headers: {
|
|
53
|
+
Accept: 'text/plain'
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!logs) {
|
|
58
|
+
console.log('\nNo logs available.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If request() parsed JSON (due to content-type), handle both string or { logs }
|
|
63
|
+
const output = typeof logs === 'string' ? logs : logs.logs || '';
|
|
64
|
+
|
|
65
|
+
console.log('\n' + output.trimEnd());
|
|
66
|
+
}
|