@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,539 @@
1
+ /**
2
+ * Profile command - manage CLI configuration profiles
3
+ *
4
+ * Profiles allow switching between different CoderFlow server configurations
5
+ * (e.g., my-server, team-name, project-x) without changing environment variables.
6
+ */
7
+
8
+ import { promises as fs } from 'fs';
9
+ import readline from 'readline';
10
+ import {
11
+ listProfiles,
12
+ loadProfile,
13
+ saveProfile,
14
+ deleteProfile,
15
+ getActiveProfileName,
16
+ setActiveProfile,
17
+ profileExists,
18
+ isValidProfileName,
19
+ getProfileConfigKeys,
20
+ createProfileFromLegacyConfig,
21
+ getProfilePath,
22
+ loadMainConfig
23
+ } from '../profile.js';
24
+ import { invalidateConfigCache } from '../config.js';
25
+
26
+ /**
27
+ * Handle profile command
28
+ * Usage:
29
+ * coder profile list List all profiles
30
+ * coder profile show [name] Show profile details
31
+ * coder profile create <name> Create new profile
32
+ * coder profile switch <name> Switch active profile
33
+ * coder profile delete <name> Delete a profile
34
+ * coder profile copy <src> <dest> Copy a profile
35
+ * coder profile set <key> <value> Set a value in active profile
36
+ * coder profile get <key> Get a value from active profile
37
+ */
38
+ export async function handleProfile(args) {
39
+ const subcommand = args[0];
40
+
41
+ if (!subcommand) {
42
+ showProfileHelp();
43
+ process.exit(1);
44
+ }
45
+
46
+ try {
47
+ switch (subcommand) {
48
+ case 'list':
49
+ case 'ls':
50
+ await listProfilesCommand();
51
+ break;
52
+
53
+ case 'show':
54
+ await showProfileCommand(args[1]);
55
+ break;
56
+
57
+ case 'create':
58
+ case 'add':
59
+ await createProfileCommand(args[1], args.slice(2));
60
+ break;
61
+
62
+ case 'switch':
63
+ case 'use':
64
+ await switchProfileCommand(args[1]);
65
+ break;
66
+
67
+ case 'delete':
68
+ case 'remove':
69
+ case 'rm':
70
+ await deleteProfileCommand(args[1]);
71
+ break;
72
+
73
+ case 'copy':
74
+ case 'cp':
75
+ await copyProfileCommand(args[1], args[2]);
76
+ break;
77
+
78
+ case 'set':
79
+ await setProfileValueCommand(args[1], args[2]);
80
+ break;
81
+
82
+ case 'get':
83
+ await getProfileValueCommand(args[1]);
84
+ break;
85
+
86
+ case 'current':
87
+ await showCurrentProfile();
88
+ break;
89
+
90
+ case 'migrate':
91
+ await migrateToProfile(args[1]);
92
+ break;
93
+
94
+ default:
95
+ console.error(`Unknown subcommand: ${subcommand}`);
96
+ showProfileHelp();
97
+ process.exit(1);
98
+ }
99
+ } catch (error) {
100
+ console.error(`Error: ${error.message}`);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ function showProfileHelp() {
106
+ console.log(`
107
+ Usage: coder profile <command> [options]
108
+
109
+ Commands:
110
+ list List all profiles (shows active profile)
111
+ show [name] Show profile details (active if no name given)
112
+ create <name> Create a new profile
113
+ switch <name> Switch to a different profile
114
+ delete <name> Delete a profile
115
+ copy <source> <dest> Copy an existing profile
116
+ set <key> <value> Set a value in the active profile
117
+ get <key> Get a value from the active profile
118
+ current Show the current active profile name
119
+ migrate <name> Create profile from current legacy config
120
+
121
+ Profile Keys:
122
+ server Server URL (e.g., http://localhost:3000)
123
+ apiKey API authentication key
124
+ default_environment Default environment name
125
+ coder_setup_path Path to coder-setup directory
126
+ server_port Server port number
127
+ profound_coder_path Path to profound-coder directory
128
+
129
+ Examples:
130
+ coder profile list
131
+ coder profile create my-server
132
+ coder profile switch my-server
133
+ coder profile set server https://coderflow.example.com
134
+ coder profile show my-server
135
+ coder profile copy my-server project-x
136
+ coder profile delete project-x
137
+
138
+ Using Profiles:
139
+ - Use 'coder profile switch <name>' to change the active profile
140
+ - Use 'coder --profile=<name> <command>' to use a profile for one command
141
+ - Set CODER_PROFILE=<name> environment variable to override
142
+ `);
143
+ }
144
+
145
+ async function listProfilesCommand() {
146
+ const profiles = await listProfiles();
147
+ const activeProfile = await getActiveProfileName();
148
+
149
+ if (profiles.length === 0) {
150
+ console.log('No profiles found.');
151
+ console.log('');
152
+ console.log('Create one with:');
153
+ console.log(' coder profile create <name>');
154
+ console.log('');
155
+ console.log('Or migrate your current config:');
156
+ console.log(' coder profile migrate default');
157
+ return;
158
+ }
159
+
160
+ console.log('Profiles:');
161
+ console.log('');
162
+
163
+ for (const name of profiles) {
164
+ const isActive = name === activeProfile;
165
+ const marker = isActive ? '*' : ' ';
166
+ const profile = await loadProfile(name);
167
+ const server = profile?.server || '(not set)';
168
+
169
+ console.log(` ${marker} ${name}`);
170
+ console.log(` Server: ${server}`);
171
+ }
172
+
173
+ console.log('');
174
+ if (activeProfile) {
175
+ console.log(`Active profile: ${activeProfile}`);
176
+ } else {
177
+ console.log('No active profile (using legacy config)');
178
+ }
179
+ }
180
+
181
+ async function showProfileCommand(profileName) {
182
+ // If no name provided, show active profile
183
+ if (!profileName) {
184
+ profileName = await getActiveProfileName();
185
+ if (!profileName) {
186
+ console.log('No active profile set.');
187
+ console.log('');
188
+ console.log('Use "coder profile list" to see available profiles.');
189
+ console.log('Use "coder profile switch <name>" to set an active profile.');
190
+ return;
191
+ }
192
+ }
193
+
194
+ const profile = await loadProfile(profileName);
195
+ if (!profile) {
196
+ console.error(`Profile '${profileName}' not found.`);
197
+ console.error('');
198
+ console.error('Use "coder profile list" to see available profiles.');
199
+ process.exit(1);
200
+ }
201
+
202
+ const activeProfile = await getActiveProfileName();
203
+ const isActive = profileName === activeProfile;
204
+
205
+ console.log(`Profile: ${profileName}${isActive ? ' (active)' : ''}`);
206
+ console.log(`Location: ${getProfilePath(profileName)}`);
207
+ console.log('');
208
+ console.log('Settings:');
209
+
210
+ for (const key of getProfileConfigKeys()) {
211
+ const value = profile[key];
212
+ if (value !== undefined) {
213
+ // Mask API key for security
214
+ const displayValue = key === 'apiKey' ? maskApiKey(value) : value;
215
+ console.log(` ${key}: ${displayValue}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ function maskApiKey(apiKey) {
221
+ if (!apiKey || apiKey.length < 8) {
222
+ return '****';
223
+ }
224
+ return apiKey.slice(0, 4) + '****' + apiKey.slice(-4);
225
+ }
226
+
227
+ async function createProfileCommand(profileName, extraArgs) {
228
+ if (!profileName) {
229
+ console.error('Usage: coder profile create <name>');
230
+ process.exit(1);
231
+ }
232
+
233
+ if (!isValidProfileName(profileName)) {
234
+ console.error(`Invalid profile name: ${profileName}`);
235
+ console.error('Profile names can only contain letters, numbers, hyphens, and underscores.');
236
+ process.exit(1);
237
+ }
238
+
239
+ if (await profileExists(profileName)) {
240
+ console.error(`Profile '${profileName}' already exists.`);
241
+ console.error('Use "coder profile delete" first, or choose a different name.');
242
+ process.exit(1);
243
+ }
244
+
245
+ // Parse extra arguments for initial values (--server=..., --apiKey=..., etc.)
246
+ const initialValues = parseProfileArgs(extraArgs);
247
+
248
+ // Create profile with initial values
249
+ const profileData = {
250
+ name: profileName,
251
+ ...initialValues
252
+ };
253
+
254
+ await saveProfile(profileName, profileData);
255
+
256
+ console.log(`Created profile: ${profileName}`);
257
+ console.log(`Location: ${getProfilePath(profileName)}`);
258
+ console.log('');
259
+
260
+ if (Object.keys(initialValues).length > 0) {
261
+ console.log('Initial settings:');
262
+ for (const [key, value] of Object.entries(initialValues)) {
263
+ const displayValue = key === 'apiKey' ? maskApiKey(value) : value;
264
+ console.log(` ${key}: ${displayValue}`);
265
+ }
266
+ console.log('');
267
+ }
268
+
269
+ console.log('To configure this profile:');
270
+ console.log(` coder profile switch ${profileName}`);
271
+ console.log(` coder profile set server <url>`);
272
+ console.log(` coder profile set apiKey <key>`);
273
+ }
274
+
275
+ function parseProfileArgs(args) {
276
+ const values = {};
277
+ const validKeys = new Set(getProfileConfigKeys());
278
+
279
+ for (const arg of args) {
280
+ if (arg.startsWith('--')) {
281
+ const equalIndex = arg.indexOf('=');
282
+ if (equalIndex > 2) {
283
+ const key = arg.slice(2, equalIndex);
284
+ const value = arg.slice(equalIndex + 1);
285
+ if (validKeys.has(key)) {
286
+ values[key] = value;
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ return values;
293
+ }
294
+
295
+ async function switchProfileCommand(profileName) {
296
+ if (!profileName) {
297
+ // No name provided - show current profile
298
+ const current = await getActiveProfileName();
299
+ if (current) {
300
+ console.log(`Current profile: ${current}`);
301
+ } else {
302
+ console.log('No active profile set (using legacy config)');
303
+ }
304
+ console.log('');
305
+ console.log('Usage: coder profile switch <name>');
306
+ return;
307
+ }
308
+
309
+ if (!await profileExists(profileName)) {
310
+ console.error(`Profile '${profileName}' not found.`);
311
+ console.error('');
312
+ console.error('Available profiles:');
313
+ const profiles = await listProfiles();
314
+ if (profiles.length === 0) {
315
+ console.error(' (none)');
316
+ } else {
317
+ for (const name of profiles) {
318
+ console.error(` - ${name}`);
319
+ }
320
+ }
321
+ process.exit(1);
322
+ }
323
+
324
+ await setActiveProfile(profileName);
325
+ invalidateConfigCache();
326
+
327
+ const profile = await loadProfile(profileName);
328
+ console.log(`Switched to profile: ${profileName}`);
329
+ if (profile?.server) {
330
+ console.log(`Server: ${profile.server}`);
331
+ }
332
+ }
333
+
334
+ async function deleteProfileCommand(profileName) {
335
+ if (!profileName) {
336
+ console.error('Usage: coder profile delete <name>');
337
+ process.exit(1);
338
+ }
339
+
340
+ if (!await profileExists(profileName)) {
341
+ console.error(`Profile '${profileName}' not found.`);
342
+ process.exit(1);
343
+ }
344
+
345
+ // Check if this is the active profile
346
+ const activeProfile = await getActiveProfileName();
347
+ if (profileName === activeProfile) {
348
+ console.log(`Warning: '${profileName}' is the current active profile.`);
349
+
350
+ // Prompt for confirmation
351
+ const confirmed = await promptConfirmation('Delete this profile?');
352
+ if (!confirmed) {
353
+ console.log('Cancelled.');
354
+ return;
355
+ }
356
+
357
+ // Clear active profile
358
+ await setActiveProfile(null);
359
+ invalidateConfigCache();
360
+ }
361
+
362
+ await deleteProfile(profileName);
363
+ console.log(`Deleted profile: ${profileName}`);
364
+ }
365
+
366
+ async function copyProfileCommand(sourceName, destName) {
367
+ if (!sourceName || !destName) {
368
+ console.error('Usage: coder profile copy <source> <destination>');
369
+ process.exit(1);
370
+ }
371
+
372
+ if (!isValidProfileName(destName)) {
373
+ console.error(`Invalid profile name: ${destName}`);
374
+ console.error('Profile names can only contain letters, numbers, hyphens, and underscores.');
375
+ process.exit(1);
376
+ }
377
+
378
+ const sourceProfile = await loadProfile(sourceName);
379
+ if (!sourceProfile) {
380
+ console.error(`Source profile '${sourceName}' not found.`);
381
+ process.exit(1);
382
+ }
383
+
384
+ if (await profileExists(destName)) {
385
+ console.error(`Destination profile '${destName}' already exists.`);
386
+ console.error('Delete it first or choose a different name.');
387
+ process.exit(1);
388
+ }
389
+
390
+ // Copy profile data
391
+ const newProfile = { ...sourceProfile, name: destName };
392
+ await saveProfile(destName, newProfile);
393
+
394
+ console.log(`Copied profile '${sourceName}' to '${destName}'`);
395
+ }
396
+
397
+ async function setProfileValueCommand(key, value) {
398
+ if (!key || value === undefined) {
399
+ console.error('Usage: coder profile set <key> <value>');
400
+ console.error('');
401
+ console.error('Available keys:');
402
+ for (const k of getProfileConfigKeys()) {
403
+ console.error(` - ${k}`);
404
+ }
405
+ process.exit(1);
406
+ }
407
+
408
+ const validKeys = new Set(getProfileConfigKeys());
409
+ if (!validKeys.has(key)) {
410
+ console.error(`Unknown key: ${key}`);
411
+ console.error('');
412
+ console.error('Available keys:');
413
+ for (const k of getProfileConfigKeys()) {
414
+ console.error(` - ${k}`);
415
+ }
416
+ process.exit(1);
417
+ }
418
+
419
+ const activeProfileName = await getActiveProfileName();
420
+ if (!activeProfileName) {
421
+ console.error('No active profile set.');
422
+ console.error('');
423
+ console.error('Either:');
424
+ console.error(' 1. Switch to a profile: coder profile switch <name>');
425
+ console.error(' 2. Create a new profile: coder profile create <name>');
426
+ console.error(' 3. Use legacy config: coder config set <key> <value>');
427
+ process.exit(1);
428
+ }
429
+
430
+ const profile = await loadProfile(activeProfileName) || { name: activeProfileName };
431
+ profile[key] = value;
432
+ await saveProfile(activeProfileName, profile);
433
+ invalidateConfigCache();
434
+
435
+ const displayValue = key === 'apiKey' ? maskApiKey(value) : value;
436
+ console.log(`Set ${key} = ${displayValue} in profile '${activeProfileName}'`);
437
+ }
438
+
439
+ async function getProfileValueCommand(key) {
440
+ if (!key) {
441
+ console.error('Usage: coder profile get <key>');
442
+ process.exit(1);
443
+ }
444
+
445
+ const validKeys = new Set(getProfileConfigKeys());
446
+ if (!validKeys.has(key)) {
447
+ console.error(`Unknown key: ${key}`);
448
+ process.exit(1);
449
+ }
450
+
451
+ const activeProfileName = await getActiveProfileName();
452
+ if (!activeProfileName) {
453
+ console.error('No active profile set.');
454
+ process.exit(1);
455
+ }
456
+
457
+ const profile = await loadProfile(activeProfileName);
458
+ if (!profile) {
459
+ console.error(`Profile '${activeProfileName}' not found.`);
460
+ process.exit(1);
461
+ }
462
+
463
+ const value = profile[key];
464
+ if (value === undefined) {
465
+ console.error(`Key '${key}' not set in profile '${activeProfileName}'`);
466
+ process.exit(1);
467
+ }
468
+
469
+ // Don't mask for get command - user wants the actual value
470
+ console.log(value);
471
+ }
472
+
473
+ async function showCurrentProfile() {
474
+ const activeProfile = await getActiveProfileName();
475
+
476
+ if (process.env.CODER_PROFILE) {
477
+ console.log(`${activeProfile} (from CODER_PROFILE env var)`);
478
+ } else if (activeProfile) {
479
+ console.log(activeProfile);
480
+ } else {
481
+ console.log('(none)');
482
+ }
483
+ }
484
+
485
+ async function migrateToProfile(profileName) {
486
+ if (!profileName) {
487
+ console.error('Usage: coder profile migrate <name>');
488
+ console.error('');
489
+ console.error('This will create a new profile from your current legacy config.');
490
+ process.exit(1);
491
+ }
492
+
493
+ if (!isValidProfileName(profileName)) {
494
+ console.error(`Invalid profile name: ${profileName}`);
495
+ process.exit(1);
496
+ }
497
+
498
+ if (await profileExists(profileName)) {
499
+ console.error(`Profile '${profileName}' already exists.`);
500
+ process.exit(1);
501
+ }
502
+
503
+ const result = await createProfileFromLegacyConfig(profileName);
504
+ if (!result) {
505
+ console.log('No legacy configuration found to migrate.');
506
+ console.log('');
507
+ console.log('Create a new profile with:');
508
+ console.log(` coder profile create ${profileName}`);
509
+ return;
510
+ }
511
+
512
+ console.log(`Created profile '${profileName}' from legacy config.`);
513
+ console.log('');
514
+ console.log('Migrated settings:');
515
+ for (const [key, value] of Object.entries(result)) {
516
+ if (key !== 'name') {
517
+ const displayValue = key === 'apiKey' ? maskApiKey(value) : value;
518
+ console.log(` ${key}: ${displayValue}`);
519
+ }
520
+ }
521
+
522
+ console.log('');
523
+ console.log('To use this profile:');
524
+ console.log(` coder profile switch ${profileName}`);
525
+ }
526
+
527
+ async function promptConfirmation(message) {
528
+ const rl = readline.createInterface({
529
+ input: process.stdin,
530
+ output: process.stdout
531
+ });
532
+
533
+ return new Promise((resolve) => {
534
+ rl.question(`${message} [y/N] `, (answer) => {
535
+ rl.close();
536
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
537
+ });
538
+ });
539
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Command: coder reject - Reject task results
3
+ */
4
+
5
+ import { request } from '../http-client.js';
6
+
7
+ function parseRejectArgs(args = []) {
8
+ const options = {
9
+ taskId: null,
10
+ cleanup: false
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 === '--cleanup') {
20
+ options.cleanup = true;
21
+ continue;
22
+ }
23
+
24
+ console.error(`Error: Unknown option ${arg}`);
25
+ console.error('Usage: coder reject <task-id> [--cleanup]');
26
+ process.exit(1);
27
+ }
28
+
29
+ return options;
30
+ }
31
+
32
+ export async function rejectTask(args = []) {
33
+ const { taskId, cleanup } = parseRejectArgs(args);
34
+
35
+ if (!taskId) {
36
+ console.error('Error: Task ID required');
37
+ console.error('Usage: coder reject <task-id> [--cleanup]');
38
+ process.exit(1);
39
+ }
40
+
41
+ console.log(`Rejecting task ${taskId}...`);
42
+
43
+ const body = cleanup ? { cleanup: true } : {};
44
+ await request(`/tasks/${taskId}/reject`, {
45
+ method: 'POST',
46
+ body: JSON.stringify(body)
47
+ });
48
+
49
+ console.log('✓ Task rejected');
50
+ if (cleanup) {
51
+ console.log('✓ Task artifacts cleaned up');
52
+ }
53
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Command: coder results - Get results of a completed task
3
+ */
4
+
5
+ import { request } from '../http-client.js';
6
+
7
+ export async function getResults(taskId) {
8
+ if (!taskId) {
9
+ console.error('Error: Task ID required');
10
+ console.error('Usage: coder results <task-id>');
11
+ process.exit(1);
12
+ }
13
+
14
+ console.log(`Fetching results for task ${taskId}...`);
15
+
16
+ const data = await request(`/tasks/${taskId}/results`);
17
+
18
+ console.log(`\nTask Results:`);
19
+ console.log(` Task ID: ${data.task.taskId}`);
20
+ console.log(` Status: ${data.task.status}`);
21
+ console.log(` Exit Code: ${data.task.exitCode}`);
22
+ console.log(` Created: ${data.task.createdAt}`);
23
+ console.log(` Finished: ${data.task.finishedAt}`);
24
+ if (data.task.environment) {
25
+ console.log(` Environment: ${data.task.environment}`);
26
+ }
27
+
28
+ if (data.task.parameters && Object.keys(data.task.parameters || {}).length > 0) {
29
+ console.log('\nParameters:');
30
+ Object.entries(data.task.parameters).forEach(([key, value]) => {
31
+ console.log(` - ${key}: ${value}`);
32
+ });
33
+ }
34
+
35
+ if (data.task.envVars && Object.keys(data.task.envVars || {}).length > 0) {
36
+ console.log('\nEnvironment Variables:');
37
+ Object.entries(data.task.envVars).forEach(([key, value]) => {
38
+ console.log(` - ${key}: ${value}`);
39
+ });
40
+ }
41
+
42
+ console.log(`\nTask Metadata (from task.json):`);
43
+ console.log(` Environment: ${data.result.environment}`);
44
+ console.log(` Task Type: ${data.result.task_type}`);
45
+ console.log(` Container: ${data.result.container_id}`);
46
+ console.log(` Command: ${data.result.command}`);
47
+
48
+ if (data.task.exitCode === 0) {
49
+ console.log(`\n✓ Task completed successfully`);
50
+ } else {
51
+ console.log(`\n✗ Task failed with exit code ${data.task.exitCode}`);
52
+ }
53
+
54
+ if (data.result.summary) {
55
+ console.log('\nSummary:');
56
+ console.log(data.result.summary.trim());
57
+ }
58
+
59
+ const repos = Array.isArray(data.result.repos_changed) ? data.result.repos_changed : [];
60
+ const stats = data.result.stats || {
61
+ repositories: repos.length,
62
+ filesChanged: repos.reduce((sum, repo) => sum + (Number(repo.files_changed) || 0), 0),
63
+ linesAdded: repos.reduce((sum, repo) => sum + (Number(repo.lines_added) || 0), 0),
64
+ linesDeleted: repos.reduce((sum, repo) => sum + (Number(repo.lines_deleted) || 0), 0)
65
+ };
66
+
67
+ if (repos.length > 0) {
68
+ console.log('\nRepositories Changed:');
69
+ repos.forEach((repo) => {
70
+ const filesChanged = Number(repo.files_changed) || (Array.isArray(repo.files) ? repo.files.length : 0);
71
+ const linesAdded = Number(repo.lines_added) || 0;
72
+ const linesDeleted = Number(repo.lines_deleted) || 0;
73
+ console.log(` • ${repo.name} (${filesChanged} files, +${linesAdded} -${linesDeleted})`);
74
+
75
+ if (Array.isArray(repo.files) && repo.files.length > 0) {
76
+ console.log(' Files:');
77
+ repo.files.forEach((file) => {
78
+ const added = Number(file.added) || 0;
79
+ const deleted = Number(file.deleted) || 0;
80
+ console.log(` - ${file.path} (+${added} -${deleted})`);
81
+ });
82
+ }
83
+ });
84
+
85
+ console.log(`\nTotals: ${stats.repositories || repos.length} repos, ${stats.filesChanged || 0} files, +${stats.linesAdded || 0} -${stats.linesDeleted || 0}`);
86
+ } else {
87
+ console.log('\nNo repositories changed.');
88
+ }
89
+ }