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