@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,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder jira - JIRA integration management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { getCoderSetupPath } from '../config.js';
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prompt for input from stdin
|
|
16
|
+
*/
|
|
17
|
+
function prompt(question) {
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question(question, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prompt for API token (visible input for easy paste verification)
|
|
33
|
+
*/
|
|
34
|
+
function promptSecret(question) {
|
|
35
|
+
// Use regular prompt - hiding prevents paste from working properly
|
|
36
|
+
// and adds no real security value for a pasted token in local CLI
|
|
37
|
+
return prompt(question);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Test JIRA connection
|
|
42
|
+
*/
|
|
43
|
+
async function testJiraConnection(baseUrl, email, apiToken) {
|
|
44
|
+
try {
|
|
45
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString('base64');
|
|
46
|
+
|
|
47
|
+
// Use node's fetch (Node 18+) or dynamic import
|
|
48
|
+
const response = await fetch(`${baseUrl}/rest/api/3/myself`, {
|
|
49
|
+
headers: {
|
|
50
|
+
'Authorization': `Basic ${auth}`,
|
|
51
|
+
'Accept': 'application/json'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
const user = await response.json();
|
|
57
|
+
return { success: true, user };
|
|
58
|
+
} else {
|
|
59
|
+
const error = await response.text();
|
|
60
|
+
return { success: false, error: `HTTP ${response.status}: ${error}` };
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { success: false, error: error.message };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Open browser to JIRA API token page
|
|
69
|
+
*/
|
|
70
|
+
async function openTokenPage() {
|
|
71
|
+
const url = 'https://id.atlassian.com/manage-profile/security/api-tokens';
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const platform = process.platform;
|
|
75
|
+
let command;
|
|
76
|
+
|
|
77
|
+
if (platform === 'darwin') {
|
|
78
|
+
command = `open "${url}"`;
|
|
79
|
+
} else if (platform === 'win32') {
|
|
80
|
+
command = `start "${url}"`;
|
|
81
|
+
} else {
|
|
82
|
+
command = `xdg-open "${url}"`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await execAsync(command);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn('Could not open browser automatically.');
|
|
89
|
+
console.log(`Please visit: ${url}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Connect to JIRA - interactive setup
|
|
96
|
+
*/
|
|
97
|
+
export async function connect(args) {
|
|
98
|
+
console.log('═══════════════════════════════════════');
|
|
99
|
+
console.log(' JIRA Integration Setup');
|
|
100
|
+
console.log('═══════════════════════════════════════');
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
// Get coder-setup path
|
|
104
|
+
let setupPath = args.setup || args['setup-path'];
|
|
105
|
+
if (!setupPath) {
|
|
106
|
+
setupPath = await getCoderSetupPath();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!setupPath) {
|
|
110
|
+
console.error('Error: No coder-setup path configured.');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log('Please specify a setup path using one of:');
|
|
113
|
+
console.log(' • coder jira connect --setup-path /path/to/setup');
|
|
114
|
+
console.log(' • coder server start --setup /path/to/setup (then try again)');
|
|
115
|
+
console.log(' • Set CODER_SETUP_PATH environment variable');
|
|
116
|
+
console.log('');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Using setup: ${setupPath}`);
|
|
121
|
+
console.log('');
|
|
122
|
+
|
|
123
|
+
// Check if setup path exists
|
|
124
|
+
try {
|
|
125
|
+
await fs.access(setupPath);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`Error: Setup path does not exist: ${setupPath}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Load existing JIRA config if any
|
|
132
|
+
const jiraConfigPath = path.join(setupPath, 'jira.json');
|
|
133
|
+
let existingConfig = null;
|
|
134
|
+
try {
|
|
135
|
+
const content = await fs.readFile(jiraConfigPath, 'utf-8');
|
|
136
|
+
existingConfig = JSON.parse(content);
|
|
137
|
+
console.log('✓ Found existing JIRA configuration');
|
|
138
|
+
console.log('');
|
|
139
|
+
} catch {
|
|
140
|
+
// No existing config
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Prompt for JIRA URL
|
|
144
|
+
console.log('Step 1/4: JIRA Base URL');
|
|
145
|
+
console.log('────────────────────────────────────────');
|
|
146
|
+
const defaultUrl = existingConfig?.baseUrl || 'https://your-company.atlassian.net';
|
|
147
|
+
const baseUrl = await prompt(`JIRA Base URL [${defaultUrl}]: `);
|
|
148
|
+
const finalBaseUrl = (baseUrl.trim() || defaultUrl).replace(/\/$/, ''); // Remove trailing slash
|
|
149
|
+
console.log('');
|
|
150
|
+
|
|
151
|
+
// Prompt for email
|
|
152
|
+
console.log('Step 2/4: JIRA Email');
|
|
153
|
+
console.log('────────────────────────────────────────');
|
|
154
|
+
const defaultEmail = existingConfig?.auth?.email || '';
|
|
155
|
+
const emailPrompt = defaultEmail ? `Email [${defaultEmail}]: ` : 'Email: ';
|
|
156
|
+
const email = await prompt(emailPrompt);
|
|
157
|
+
const finalEmail = email.trim() || defaultEmail;
|
|
158
|
+
|
|
159
|
+
if (!finalEmail) {
|
|
160
|
+
console.error('Error: Email is required');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
console.log('');
|
|
164
|
+
|
|
165
|
+
// Prompt for API token
|
|
166
|
+
console.log('Step 3/4: JIRA API Token');
|
|
167
|
+
console.log('────────────────────────────────────────');
|
|
168
|
+
console.log('You need a personal API token from JIRA.');
|
|
169
|
+
console.log('');
|
|
170
|
+
|
|
171
|
+
const shouldOpen = await prompt('Open browser to get API token? [Y/n]: ');
|
|
172
|
+
if (!shouldOpen.trim() || shouldOpen.toLowerCase() === 'y' || shouldOpen.toLowerCase() === 'yes') {
|
|
173
|
+
console.log('Opening browser...');
|
|
174
|
+
await openTokenPage();
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log('Instructions:');
|
|
177
|
+
console.log(' 1. Click "Create API token"');
|
|
178
|
+
console.log(' 2. Give it a name (e.g., "CoderFlow")');
|
|
179
|
+
console.log(' 3. Copy the token');
|
|
180
|
+
console.log(' 4. Paste it below');
|
|
181
|
+
console.log('');
|
|
182
|
+
} else {
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log('Get your API token at:');
|
|
185
|
+
console.log('https://id.atlassian.com/manage-profile/security/api-tokens');
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const apiToken = await promptSecret('API Token: ');
|
|
190
|
+
if (!apiToken.trim()) {
|
|
191
|
+
console.error('Error: API Token is required');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
console.log('');
|
|
195
|
+
|
|
196
|
+
// Test connection
|
|
197
|
+
console.log('Step 4/4: Testing Connection');
|
|
198
|
+
console.log('────────────────────────────────────────');
|
|
199
|
+
console.log('Connecting to JIRA...');
|
|
200
|
+
|
|
201
|
+
const testResult = await testJiraConnection(finalBaseUrl, finalEmail, apiToken.trim());
|
|
202
|
+
|
|
203
|
+
if (!testResult.success) {
|
|
204
|
+
console.error('✗ Connection failed');
|
|
205
|
+
console.error('');
|
|
206
|
+
console.error('Error:', testResult.error);
|
|
207
|
+
console.error('');
|
|
208
|
+
console.error('Please check your credentials and try again.');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(`✓ Connected successfully as ${testResult.user.displayName || finalEmail}`);
|
|
213
|
+
console.log('');
|
|
214
|
+
|
|
215
|
+
// Save configuration
|
|
216
|
+
const jiraConfig = {
|
|
217
|
+
enabled: true,
|
|
218
|
+
baseUrl: finalBaseUrl,
|
|
219
|
+
auth: {
|
|
220
|
+
email: finalEmail,
|
|
221
|
+
apiToken: apiToken.trim()
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await fs.writeFile(jiraConfigPath, JSON.stringify(jiraConfig, null, 2), 'utf-8');
|
|
227
|
+
console.log('✓ Configuration saved');
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(`Saved to: ${jiraConfigPath}`);
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log('═══════════════════════════════════════');
|
|
232
|
+
console.log('✓ JIRA integration is now configured!');
|
|
233
|
+
console.log('═══════════════════════════════════════');
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log('The server will automatically use this configuration.');
|
|
236
|
+
console.log('No environment variables needed!');
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('Next steps:');
|
|
239
|
+
console.log(' • Start/restart the server: coder server start');
|
|
240
|
+
console.log(' • Open the web UI and look for "Import from JIRA"');
|
|
241
|
+
console.log('');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error('✗ Failed to save configuration');
|
|
244
|
+
console.error('');
|
|
245
|
+
console.error('Error:', error.message);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Show JIRA connection status
|
|
252
|
+
*/
|
|
253
|
+
export async function status(args) {
|
|
254
|
+
let setupPath = args.setup || args['setup-path'];
|
|
255
|
+
if (!setupPath) {
|
|
256
|
+
setupPath = await getCoderSetupPath();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!setupPath) {
|
|
260
|
+
console.log('JIRA Status: Not configured');
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log('Run: coder jira connect --setup-path /path/to/setup');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const jiraConfigPath = path.join(setupPath, 'jira.json');
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const content = await fs.readFile(jiraConfigPath, 'utf-8');
|
|
270
|
+
const config = JSON.parse(content);
|
|
271
|
+
|
|
272
|
+
console.log('JIRA Status: Configured ✓');
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(` Base URL: ${config.baseUrl}`);
|
|
275
|
+
console.log(` Email: ${config.auth?.email || 'Not set'}`);
|
|
276
|
+
|
|
277
|
+
// Show first and last few chars to help verify token is correct
|
|
278
|
+
const token = config.auth?.apiToken || '';
|
|
279
|
+
const tokenDisplay = token.length > 10
|
|
280
|
+
? `${token.substring(0, 8)}...${token.substring(token.length - 8)}`
|
|
281
|
+
: (token ? '*'.repeat(token.length) : 'Not set');
|
|
282
|
+
console.log(` Token: ${tokenDisplay}`);
|
|
283
|
+
|
|
284
|
+
console.log(` Enabled: ${config.enabled ? 'Yes' : 'No'}`);
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(`Config file: ${jiraConfigPath}`);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error.code === 'ENOENT') {
|
|
289
|
+
console.log('JIRA Status: Not configured');
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log('Run: coder jira connect');
|
|
292
|
+
} else {
|
|
293
|
+
console.error('Error reading JIRA configuration:', error.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Disable JIRA integration
|
|
300
|
+
*/
|
|
301
|
+
export async function disable(args) {
|
|
302
|
+
let setupPath = args.setup || args['setup-path'];
|
|
303
|
+
if (!setupPath) {
|
|
304
|
+
setupPath = await getCoderSetupPath();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!setupPath) {
|
|
308
|
+
console.error('Error: No coder-setup path configured.');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const jiraConfigPath = path.join(setupPath, 'jira.json');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const content = await fs.readFile(jiraConfigPath, 'utf-8');
|
|
316
|
+
const config = JSON.parse(content);
|
|
317
|
+
config.enabled = false;
|
|
318
|
+
await fs.writeFile(jiraConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
319
|
+
|
|
320
|
+
console.log('✓ JIRA integration disabled');
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log('To re-enable: coder jira enable');
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error.code === 'ENOENT') {
|
|
325
|
+
console.log('JIRA is not configured yet.');
|
|
326
|
+
} else {
|
|
327
|
+
console.error('Error:', error.message);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Enable JIRA integration
|
|
335
|
+
*/
|
|
336
|
+
export async function enable(args) {
|
|
337
|
+
let setupPath = args.setup || args['setup-path'];
|
|
338
|
+
if (!setupPath) {
|
|
339
|
+
setupPath = await getCoderSetupPath();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!setupPath) {
|
|
343
|
+
console.error('Error: No coder-setup path configured.');
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const jiraConfigPath = path.join(setupPath, 'jira.json');
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const content = await fs.readFile(jiraConfigPath, 'utf-8');
|
|
351
|
+
const config = JSON.parse(content);
|
|
352
|
+
config.enabled = true;
|
|
353
|
+
await fs.writeFile(jiraConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
354
|
+
|
|
355
|
+
console.log('✓ JIRA integration enabled');
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error.code === 'ENOENT') {
|
|
358
|
+
console.log('JIRA is not configured yet.');
|
|
359
|
+
console.log('');
|
|
360
|
+
console.log('Run: coder jira connect');
|
|
361
|
+
} else {
|
|
362
|
+
console.error('Error:', error.message);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Test JIRA connection with saved configuration
|
|
370
|
+
*/
|
|
371
|
+
export async function test(args) {
|
|
372
|
+
let setupPath = args.setup || args['setup-path'];
|
|
373
|
+
if (!setupPath) {
|
|
374
|
+
setupPath = await getCoderSetupPath();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!setupPath) {
|
|
378
|
+
console.error('Error: No coder-setup path configured.');
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const jiraConfigPath = path.join(setupPath, 'jira.json');
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const content = await fs.readFile(jiraConfigPath, 'utf-8');
|
|
386
|
+
const config = JSON.parse(content);
|
|
387
|
+
|
|
388
|
+
if (!config.enabled) {
|
|
389
|
+
console.log('JIRA integration is disabled.');
|
|
390
|
+
console.log('Run: coder jira enable');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log('Testing JIRA connection...');
|
|
395
|
+
console.log('');
|
|
396
|
+
|
|
397
|
+
const testResult = await testJiraConnection(config.baseUrl, config.auth.email, config.auth.apiToken);
|
|
398
|
+
|
|
399
|
+
if (testResult.success) {
|
|
400
|
+
console.log('✓ Connection successful!');
|
|
401
|
+
console.log(` User: ${testResult.user.displayName || config.auth.email}`);
|
|
402
|
+
console.log('');
|
|
403
|
+
} else {
|
|
404
|
+
console.error('✗ Connection failed');
|
|
405
|
+
console.error('');
|
|
406
|
+
console.error('Error:', testResult.error);
|
|
407
|
+
console.error('');
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error.code === 'ENOENT') {
|
|
412
|
+
console.log('JIRA is not configured yet.');
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log('Run: coder jira connect');
|
|
415
|
+
} else {
|
|
416
|
+
console.error('Error:', error.message);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Main jira command handler
|
|
424
|
+
*/
|
|
425
|
+
export async function jira(args) {
|
|
426
|
+
const subcommand = args._[0];
|
|
427
|
+
|
|
428
|
+
switch (subcommand) {
|
|
429
|
+
case 'connect':
|
|
430
|
+
await connect(args);
|
|
431
|
+
break;
|
|
432
|
+
case 'status':
|
|
433
|
+
await status(args);
|
|
434
|
+
break;
|
|
435
|
+
case 'test':
|
|
436
|
+
await test(args);
|
|
437
|
+
break;
|
|
438
|
+
case 'disable':
|
|
439
|
+
await disable(args);
|
|
440
|
+
break;
|
|
441
|
+
case 'enable':
|
|
442
|
+
await enable(args);
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
console.log('Usage: coder jira <command>');
|
|
446
|
+
console.log('');
|
|
447
|
+
console.log('Commands:');
|
|
448
|
+
console.log(' connect Set up JIRA integration (interactive)');
|
|
449
|
+
console.log(' status Show JIRA configuration status');
|
|
450
|
+
console.log(' test Test JIRA connection');
|
|
451
|
+
console.log(' enable Enable JIRA integration');
|
|
452
|
+
console.log(' disable Disable JIRA integration');
|
|
453
|
+
console.log('');
|
|
454
|
+
console.log('Options:');
|
|
455
|
+
console.log(' --setup-path <path> Path to coder-setup directory');
|
|
456
|
+
console.log('');
|
|
457
|
+
console.log('Examples:');
|
|
458
|
+
console.log(' coder jira connect');
|
|
459
|
+
console.log(' coder jira status');
|
|
460
|
+
console.log(' coder jira test');
|
|
461
|
+
console.log(' coder jira connect --setup-path /path/to/setup');
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License command - manage CoderFlow license
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { getCoderSetupPath } from '../config.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Handle license command
|
|
11
|
+
* Usage:
|
|
12
|
+
* coder license set <key>
|
|
13
|
+
* coder license show
|
|
14
|
+
* coder license verify
|
|
15
|
+
*/
|
|
16
|
+
export async function handleLicense(args) {
|
|
17
|
+
const subcommand = args[0];
|
|
18
|
+
|
|
19
|
+
if (!subcommand) {
|
|
20
|
+
console.error('Usage: coder license <set|show|verify> [key]');
|
|
21
|
+
console.error('');
|
|
22
|
+
console.error('Commands:');
|
|
23
|
+
console.error(' set <key> Install a license key');
|
|
24
|
+
console.error(' show Show current license status');
|
|
25
|
+
console.error(' verify Verify the license with the server');
|
|
26
|
+
console.error('');
|
|
27
|
+
console.error('Example:');
|
|
28
|
+
console.error(' coder license set <your-license-key>');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
switch (subcommand) {
|
|
34
|
+
case 'set':
|
|
35
|
+
await setLicense(args[1]);
|
|
36
|
+
break;
|
|
37
|
+
|
|
38
|
+
case 'show':
|
|
39
|
+
await showLicense();
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
case 'verify':
|
|
43
|
+
await verifyLicense();
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
default:
|
|
47
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
48
|
+
console.error('Run "coder license" for usage information');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`Error: ${error.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function setLicense(key) {
|
|
58
|
+
if (!key) {
|
|
59
|
+
console.error('Usage: coder license set <key>');
|
|
60
|
+
console.error('');
|
|
61
|
+
console.error('Example:');
|
|
62
|
+
console.error(' coder license set <your-license-key>');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const setupPath = await getCoderSetupPath();
|
|
67
|
+
if (!setupPath) {
|
|
68
|
+
console.error('Error: CODER_SETUP_PATH not set');
|
|
69
|
+
console.error('');
|
|
70
|
+
console.error('The license file must be saved to your coder-setup directory.');
|
|
71
|
+
console.error('Set it with:');
|
|
72
|
+
console.error(' export CODER_SETUP_PATH=/path/to/coder-setup');
|
|
73
|
+
console.error('Or save to config:');
|
|
74
|
+
console.error(' coder server init /path/to/coder-setup');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const licensePath = path.join(setupPath, 'license.json');
|
|
79
|
+
|
|
80
|
+
// Create license data
|
|
81
|
+
const licenseData = {
|
|
82
|
+
key: key.trim().toUpperCase(),
|
|
83
|
+
installedAt: new Date().toISOString()
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Write license file
|
|
87
|
+
await fs.writeFile(licensePath, JSON.stringify(licenseData, null, 2), 'utf-8');
|
|
88
|
+
|
|
89
|
+
console.log('License installed successfully');
|
|
90
|
+
console.log(` Location: ${licensePath}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log('Restart the CoderFlow server for the license to take effect.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function showLicense() {
|
|
96
|
+
const setupPath = await getCoderSetupPath();
|
|
97
|
+
if (!setupPath) {
|
|
98
|
+
console.error('Error: CODER_SETUP_PATH not set');
|
|
99
|
+
console.error('');
|
|
100
|
+
console.error('Set it with:');
|
|
101
|
+
console.error(' export CODER_SETUP_PATH=/path/to/coder-setup');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const licensePath = path.join(setupPath, 'license.json');
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const content = await fs.readFile(licensePath, 'utf-8');
|
|
109
|
+
const licenseData = JSON.parse(content);
|
|
110
|
+
|
|
111
|
+
console.log('=== License Information ===');
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
114
|
+
// Mask the license key for display (show first and last segments)
|
|
115
|
+
const keyParts = licenseData.key.split('-');
|
|
116
|
+
let maskedKey;
|
|
117
|
+
if (keyParts.length >= 3) {
|
|
118
|
+
maskedKey = keyParts[0] + '-****-****-****-' + keyParts[keyParts.length - 1];
|
|
119
|
+
} else {
|
|
120
|
+
maskedKey = licenseData.key.slice(0, 4) + '****' + licenseData.key.slice(-4);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`Key: ${maskedKey}`);
|
|
124
|
+
console.log(`Installed: ${licenseData.installedAt}`);
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log(`Location: ${licensePath}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log('Use "coder license verify" to check license validity with the server.');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.code === 'ENOENT') {
|
|
131
|
+
console.log('No license installed');
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log('Install a license with:');
|
|
134
|
+
console.log(' coder license set <key>');
|
|
135
|
+
} else {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function verifyLicense() {
|
|
142
|
+
try {
|
|
143
|
+
const { request } = await import('../http-client.js');
|
|
144
|
+
|
|
145
|
+
// Make a simple request to the server - if license is invalid, server won't start
|
|
146
|
+
// or will return 403 on protected routes
|
|
147
|
+
const health = await request('/health');
|
|
148
|
+
|
|
149
|
+
console.log('License status: Valid');
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log('The server is running and accepting requests.');
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error.message.includes('License') || error.message.includes('403')) {
|
|
154
|
+
console.error('License status: Invalid or Expired');
|
|
155
|
+
console.error('');
|
|
156
|
+
console.error(error.message);
|
|
157
|
+
console.error('');
|
|
158
|
+
console.error('Please contact Profound Logic to obtain a valid CoderFlow license.');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
} else if (error.message.includes('ECONNREFUSED') || error.message.includes('connect')) {
|
|
161
|
+
console.error('Cannot verify license: Server is not running');
|
|
162
|
+
console.error('');
|
|
163
|
+
console.error('Start the server with:');
|
|
164
|
+
console.error(' coder server start');
|
|
165
|
+
console.error('');
|
|
166
|
+
console.error('If the server fails to start, check the license status in server logs.');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
} else {
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|