hale-commenting-system 2.2.3 → 2.2.5

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.
Files changed (2) hide show
  1. package/package.json +9 -5
  2. package/scripts/integrate.js +1136 -17
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "hale-commenting-system",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "An open source build scaffolding utility for web apps.",
5
5
  "repository": "https://github.com/patternfly/patternfly-react-seed.git",
6
6
  "homepage": "https://patternfly-react-seed.surge.sh",
7
7
  "license": "MIT",
8
+ "main": "src/app/commenting-system/index.ts",
9
+ "types": "src/app/commenting-system/index.ts",
8
10
  "bin": {
9
11
  "hale-commenting-system": "./scripts/integrate.js"
10
12
  },
@@ -70,14 +72,16 @@
70
72
  "webpack-merge": "^6.0.1"
71
73
  },
72
74
  "dependencies": {
75
+ "@babel/generator": "^7.23.0",
76
+ "@babel/parser": "^7.23.0",
77
+ "@babel/traverse": "^7.23.0",
78
+ "@babel/types": "^7.23.0",
73
79
  "@patternfly/react-core": "^6.4.0",
74
80
  "@patternfly/react-icons": "^6.4.0",
75
81
  "@patternfly/react-styles": "^6.4.0",
82
+ "inquirer": "^8.2.6",
83
+ "node-fetch": "^2.7.0",
76
84
  "react": "^18",
77
- "@babel/parser": "^7.23.0",
78
- "@babel/traverse": "^7.23.0",
79
- "@babel/generator": "^7.23.0",
80
- "@babel/types": "^7.23.0",
81
85
  "react-dom": "^18",
82
86
  "sirv-cli": "^3.0.0"
83
87
  },
@@ -11,6 +11,27 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const { execSync } = require('child_process');
13
13
 
14
+ // Check Node.js version (fetch is only available natively in Node 18+)
15
+ function checkNodeVersion() {
16
+ const nodeVersion = process.version;
17
+ const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
18
+
19
+ if (majorVersion < 18) {
20
+ console.error('❌ Error: Node.js version 18 or higher is required.');
21
+ console.error(` Current version: ${nodeVersion}`);
22
+ console.error(' The webpack middleware uses native fetch() which requires Node 18+.');
23
+ console.error(' Please upgrade Node.js: https://nodejs.org/\n');
24
+ process.exit(1);
25
+ }
26
+
27
+ if (majorVersion === 18) {
28
+ console.log('⚠️ Warning: Node.js 18 detected. Some features may work better with Node 20+.\n');
29
+ }
30
+ }
31
+
32
+ // Run version check immediately
33
+ checkNodeVersion();
34
+
14
35
  // Check if required dependencies are available
15
36
  let parser, traverse, generate, types;
16
37
  try {
@@ -27,6 +48,23 @@ try {
27
48
 
28
49
  const readline = require('readline');
29
50
 
51
+ // Check for inquirer (better prompts)
52
+ let inquirer;
53
+ try {
54
+ inquirer = require('inquirer');
55
+ } catch (e) {
56
+ // Fallback to basic readline if inquirer not available
57
+ inquirer = null;
58
+ }
59
+
60
+ // Check for node-fetch (for API validation)
61
+ let fetch;
62
+ try {
63
+ fetch = require('node-fetch');
64
+ } catch (e) {
65
+ fetch = null;
66
+ }
67
+
30
68
  const rl = readline.createInterface({
31
69
  input: process.stdin,
32
70
  output: process.stdout
@@ -38,6 +76,70 @@ function question(prompt) {
38
76
  });
39
77
  }
40
78
 
79
+ // Use inquirer if available, otherwise fallback to basic question
80
+ async function prompt(questions) {
81
+ if (inquirer) {
82
+ return await inquirer.prompt(questions);
83
+ }
84
+ // Fallback implementation for basic prompts
85
+ const result = {};
86
+ for (const q of questions) {
87
+ let valid = false;
88
+ let answer;
89
+
90
+ while (!valid) {
91
+ if (q.type === 'list') {
92
+ console.log(`\n${q.message}:`);
93
+ q.choices.forEach((choice, idx) => {
94
+ const name = typeof choice === 'string' ? choice : choice.name;
95
+ console.log(` ${idx + 1}. ${name}`);
96
+ });
97
+ answer = await question(`Select (1-${q.choices.length}): `);
98
+ const idx = parseInt(answer) - 1;
99
+ if (idx >= 0 && idx < q.choices.length) {
100
+ result[q.name] = q.choices[idx]?.value || q.choices[idx];
101
+ valid = true;
102
+ } else {
103
+ console.log(' ❌ Invalid selection. Please try again.');
104
+ }
105
+ } else if (q.type === 'confirm') {
106
+ answer = await question(`${q.message} (Y/n): `);
107
+ result[q.name] = answer.toLowerCase() !== 'n' && answer.toLowerCase() !== 'no';
108
+ valid = true;
109
+ } else if (q.type === 'password') {
110
+ answer = await question(`${q.message}: `);
111
+ result[q.name] = answer;
112
+ if (q.validate) {
113
+ const validation = q.validate(answer);
114
+ if (validation === true) {
115
+ valid = true;
116
+ } else {
117
+ console.log(` ❌ ${validation}`);
118
+ }
119
+ } else {
120
+ valid = true;
121
+ }
122
+ } else {
123
+ answer = await question(`${q.message}${q.default ? ` (${q.default})` : ''}: `);
124
+ const value = answer.trim() || q.default || '';
125
+ if (q.validate) {
126
+ const validation = q.validate(value);
127
+ if (validation === true) {
128
+ result[q.name] = value;
129
+ valid = true;
130
+ } else {
131
+ console.log(` ❌ ${validation}`);
132
+ }
133
+ } else {
134
+ result[q.name] = value;
135
+ valid = true;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+
41
143
  function findFile(filename, startDir = process.cwd()) {
42
144
  const possiblePaths = [
43
145
  path.join(startDir, filename),
@@ -55,6 +157,657 @@ function findFile(filename, startDir = process.cwd()) {
55
157
  return null;
56
158
  }
57
159
 
160
+ // ============================================================================
161
+ // Detection Functions
162
+ // ============================================================================
163
+
164
+ function detectPatternFlySeed() {
165
+ const cwd = process.cwd();
166
+
167
+ // Check for webpack config files
168
+ const hasWebpack =
169
+ fs.existsSync(path.join(cwd, 'webpack.config.js')) ||
170
+ fs.existsSync(path.join(cwd, 'webpack.dev.js')) ||
171
+ fs.existsSync(path.join(cwd, 'webpack.common.js'));
172
+
173
+ // Check for src/app directory
174
+ const hasAppDir = fs.existsSync(path.join(cwd, 'src', 'app'));
175
+
176
+ // Check for PatternFly dependencies in package.json
177
+ let hasPatternFly = false;
178
+ try {
179
+ const packageJsonPath = path.join(cwd, 'package.json');
180
+ if (fs.existsSync(packageJsonPath)) {
181
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
182
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
183
+ hasPatternFly = !!(
184
+ deps['@patternfly/react-core'] ||
185
+ deps['@patternfly/react-icons']
186
+ );
187
+ }
188
+ } catch {
189
+ // Ignore errors
190
+ }
191
+
192
+ return hasWebpack && hasAppDir && hasPatternFly;
193
+ }
194
+
195
+ function detectGitRemote() {
196
+ const cwd = process.cwd();
197
+
198
+ // Check if .git exists
199
+ if (!fs.existsSync(path.join(cwd, '.git'))) {
200
+ return null;
201
+ }
202
+
203
+ try {
204
+ // Get remote URL
205
+ const remoteUrl = execSync('git remote get-url origin', {
206
+ cwd,
207
+ encoding: 'utf-8',
208
+ stdio: ['ignore', 'pipe', 'ignore']
209
+ }).trim();
210
+
211
+ if (!remoteUrl) {
212
+ return null;
213
+ }
214
+
215
+ // Parse GitHub URL (supports https://, git@, and ssh formats)
216
+ const githubMatch = remoteUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?$/);
217
+
218
+ if (githubMatch) {
219
+ const owner = githubMatch[1];
220
+ const repo = githubMatch[2].replace(/\.git$/, '');
221
+
222
+ // Try to detect if it's a fork by checking if upstream exists
223
+ let isFork = false;
224
+ try {
225
+ execSync('git remote get-url upstream', {
226
+ cwd,
227
+ encoding: 'utf-8',
228
+ stdio: ['ignore', 'pipe', 'ignore']
229
+ });
230
+ isFork = true;
231
+ } catch {
232
+ // Check if repo name matches patternfly-react-seed (likely a fork)
233
+ isFork = repo.includes('patternfly-react-seed') || repo.includes('pfseed');
234
+ }
235
+
236
+ return {
237
+ owner,
238
+ repo,
239
+ url: remoteUrl,
240
+ isFork
241
+ };
242
+ }
243
+ } catch (error) {
244
+ // Git command failed or not a git repo
245
+ return null;
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ function detectProjectSetup() {
252
+ const gitInfo = detectGitRemote();
253
+
254
+ if (!gitInfo) {
255
+ return 'none';
256
+ }
257
+
258
+ // Check if it looks like a fork (has patternfly-react-seed in name or has upstream)
259
+ if (gitInfo.isFork || gitInfo.repo?.includes('patternfly-react-seed')) {
260
+ return 'forked';
261
+ }
262
+
263
+ // Check if it's a clone of the original
264
+ if (gitInfo.owner === 'patternfly' && gitInfo.repo === 'patternfly-react-seed') {
265
+ return 'cloned';
266
+ }
267
+
268
+ // Has git remote but unclear
269
+ return 'unknown';
270
+ }
271
+
272
+ // ============================================================================
273
+ // Validation Functions
274
+ // ============================================================================
275
+
276
+ async function validateGitHubCredentials(clientId, clientSecret, owner, repo) {
277
+ if (!fetch) {
278
+ console.log(' ⚠️ node-fetch not available, skipping validation');
279
+ return true; // Skip validation if fetch not available
280
+ }
281
+
282
+ try {
283
+ const repoUrl = `https://api.github.com/repos/${owner}/${repo}`;
284
+ const response = await fetch(repoUrl, {
285
+ headers: {
286
+ 'Accept': 'application/vnd.github+json',
287
+ 'User-Agent': 'hale-commenting-system'
288
+ }
289
+ });
290
+
291
+ if (response.ok) {
292
+ return true;
293
+ }
294
+
295
+ if (response.status === 404) {
296
+ console.error(` Repository ${owner}/${repo} not found or not accessible`);
297
+ return false;
298
+ }
299
+
300
+ console.error(` GitHub API error: ${response.status}`);
301
+ return false;
302
+ } catch (error) {
303
+ console.error(` Error validating GitHub: ${error.message}`);
304
+ return false;
305
+ }
306
+ }
307
+
308
+ async function validateJiraCredentials(baseUrl, apiToken, email) {
309
+ if (!fetch) {
310
+ console.log(' ⚠️ node-fetch not available, skipping validation');
311
+ return true; // Skip validation if fetch not available
312
+ }
313
+
314
+ try {
315
+ const url = `${baseUrl.replace(/\/+$/, '')}/rest/api/2/myself`;
316
+
317
+ const authHeader = email
318
+ ? `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`
319
+ : `Bearer ${apiToken}`;
320
+
321
+ const response = await fetch(url, {
322
+ headers: {
323
+ 'Accept': 'application/json',
324
+ 'Authorization': authHeader,
325
+ 'User-Agent': 'hale-commenting-system'
326
+ }
327
+ });
328
+
329
+ if (response.ok) {
330
+ const data = await response.json();
331
+ console.log(` ✅ Authenticated as: ${data.displayName || data.name || 'User'}`);
332
+ return true;
333
+ }
334
+
335
+ if (response.status === 401 || response.status === 403) {
336
+ console.error(` Authentication failed. Check your token and email (if required).`);
337
+ return false;
338
+ }
339
+
340
+ console.error(` Jira API error: ${response.status}`);
341
+ return false;
342
+ } catch (error) {
343
+ console.error(` Error validating Jira: ${error.message}`);
344
+ return false;
345
+ }
346
+ }
347
+
348
+ // ============================================================================
349
+ // File Generation Functions
350
+ // ============================================================================
351
+
352
+ function generateFiles(config) {
353
+ const cwd = process.cwd();
354
+
355
+ // Generate .env file (client-safe)
356
+ const envPath = path.join(cwd, '.env');
357
+
358
+ let envContent = `# Hale Commenting System Configuration
359
+ # Client-safe environment variables (these are exposed to the browser)
360
+
361
+ `;
362
+
363
+ if (config.github && config.github.clientId) {
364
+ envContent += `# GitHub OAuth (client-side; safe to expose)
365
+ # Get your Client ID from: https://github.com/settings/developers
366
+ # 1. Click "New OAuth App"
367
+ # 2. Fill in the form (Homepage: http://localhost:9000, Callback: http://localhost:9000/api/github-oauth-callback)
368
+ # 3. Copy the Client ID
369
+ VITE_GITHUB_CLIENT_ID=${config.github.clientId}
370
+
371
+ # Target repo for Issues/Comments
372
+ VITE_GITHUB_OWNER=${config.github.owner || config.owner}
373
+ VITE_GITHUB_REPO=${config.github.repo || config.repo}
374
+
375
+ `;
376
+ } else {
377
+ envContent += `# GitHub OAuth (client-side; safe to expose)
378
+ # Get your Client ID from: https://github.com/settings/developers
379
+ # 1. Click "New OAuth App"
380
+ # 2. Fill in the form (Homepage: http://localhost:9000, Callback: http://localhost:9000/api/github-oauth-callback)
381
+ # 3. Copy the Client ID
382
+ VITE_GITHUB_CLIENT_ID=
383
+
384
+ # Target repo for Issues/Comments
385
+ VITE_GITHUB_OWNER=${config.owner}
386
+ VITE_GITHUB_REPO=${config.repo}
387
+
388
+ `;
389
+ }
390
+
391
+ if (config.jira && config.jira.baseUrl) {
392
+ envContent += `# Jira Base URL
393
+ # For Red Hat Jira, use: https://issues.redhat.com
394
+ VITE_JIRA_BASE_URL=${config.jira.baseUrl}
395
+ `;
396
+ } else {
397
+ envContent += `# Jira Base URL
398
+ # For Red Hat Jira, use: https://issues.redhat.com
399
+ VITE_JIRA_BASE_URL=
400
+ `;
401
+ }
402
+
403
+ // Check if .env exists and append or create
404
+ if (fs.existsSync(envPath)) {
405
+ const existing = fs.readFileSync(envPath, 'utf-8');
406
+ // Only add if not already present
407
+ if (!existing.includes('VITE_GITHUB_CLIENT_ID')) {
408
+ fs.appendFileSync(envPath, '\n' + envContent);
409
+ console.log(' ✅ Updated .env file');
410
+ } else {
411
+ console.log(' ⚠️ .env already contains commenting system config');
412
+ }
413
+ } else {
414
+ fs.writeFileSync(envPath, envContent);
415
+ console.log(' ✅ Created .env file');
416
+ }
417
+
418
+ // Note about empty values
419
+ if (!config.github || !config.jira) {
420
+ console.log(' ℹ️ Some values are empty - see comments in .env for setup instructions');
421
+ }
422
+
423
+ // Generate .env.server file (secrets)
424
+ const envServerPath = path.join(cwd, '.env.server');
425
+
426
+ let envServerContent = `# Hale Commenting System - Server Secrets
427
+ # ⚠️ DO NOT COMMIT THIS FILE - It contains sensitive credentials
428
+ # This file is automatically added to .gitignore
429
+
430
+ `;
431
+
432
+ if (config.github && config.github.clientSecret) {
433
+ envServerContent += `# GitHub OAuth Client Secret (server-only)
434
+ # Get this from your GitHub OAuth App settings: https://github.com/settings/developers
435
+ # Click on your OAuth App, then "Generate a new client secret"
436
+ GITHUB_CLIENT_SECRET=${config.github.clientSecret}
437
+
438
+ `;
439
+ } else {
440
+ envServerContent += `# GitHub OAuth Client Secret (server-only)
441
+ # Get this from your GitHub OAuth App settings: https://github.com/settings/developers
442
+ # Click on your OAuth App, then "Generate a new client secret"
443
+ GITHUB_CLIENT_SECRET=
444
+
445
+ `;
446
+ }
447
+
448
+ if (config.jira && config.jira.apiToken) {
449
+ envServerContent += `# Jira API Token (server-only)
450
+ # For Red Hat Jira, generate a Personal Access Token:
451
+ # 1. Visit: https://issues.redhat.com/secure/ViewProfile.jspa
452
+ # 2. Click "Personal Access Tokens" in the left sidebar
453
+ # 3. Click "Create token"
454
+ # 4. Give it a name and remove expiration
455
+ # 5. Copy the token
456
+ JIRA_API_TOKEN=${config.jira.apiToken}
457
+ `;
458
+ } else {
459
+ envServerContent += `# Jira API Token (server-only)
460
+ # For Red Hat Jira, generate a Personal Access Token:
461
+ # 1. Visit: https://issues.redhat.com/secure/ViewProfile.jspa
462
+ # 2. Click "Personal Access Tokens" in the left sidebar
463
+ # 3. Click "Create token"
464
+ # 4. Give it a name and remove expiration
465
+ # 5. Copy the token
466
+ JIRA_API_TOKEN=
467
+ `;
468
+ }
469
+
470
+ if (config.jira && config.jira.email) {
471
+ envServerContent += `JIRA_EMAIL=${config.jira.email}\n`;
472
+ }
473
+
474
+ if (fs.existsSync(envServerPath)) {
475
+ const existing = fs.readFileSync(envServerPath, 'utf-8');
476
+ if (!existing.includes('GITHUB_CLIENT_SECRET')) {
477
+ fs.appendFileSync(envServerPath, '\n' + envServerContent);
478
+ console.log(' ✅ Updated .env.server file');
479
+ } else {
480
+ console.log(' ⚠️ .env.server already contains commenting system config');
481
+ }
482
+ } else {
483
+ fs.writeFileSync(envServerPath, envServerContent);
484
+ console.log(' ✅ Created .env.server file');
485
+ }
486
+
487
+ // Note about empty values
488
+ if (!config.github || !config.jira) {
489
+ console.log(' ℹ️ Some values are empty - see comments in .env.server for setup instructions');
490
+ }
491
+
492
+ // Ensure .env.server is in .gitignore
493
+ const gitignorePath = path.join(cwd, '.gitignore');
494
+ if (fs.existsSync(gitignorePath)) {
495
+ const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
496
+ if (!gitignore.includes('.env.server')) {
497
+ fs.appendFileSync(gitignorePath, '\n.env.server\n');
498
+ console.log(' ✅ Added .env.server to .gitignore');
499
+ }
500
+ } else {
501
+ fs.writeFileSync(gitignorePath, '.env.server\n');
502
+ console.log(' ✅ Created .gitignore with .env.server');
503
+ }
504
+ }
505
+
506
+ function integrateWebpackMiddleware() {
507
+ const cwd = process.cwd();
508
+ const webpackDevPath = path.join(cwd, 'webpack.dev.js');
509
+
510
+ if (!fs.existsSync(webpackDevPath)) {
511
+ console.log(' ⚠️ webpack.dev.js not found. Cannot auto-integrate.');
512
+ return;
513
+ }
514
+
515
+ // Read webpack.dev.js
516
+ let webpackContent = fs.readFileSync(webpackDevPath, 'utf-8');
517
+
518
+ // Check if already integrated
519
+ if (webpackContent.includes('/api/github-oauth-callback') || webpackContent.includes('/api/jira-issue')) {
520
+ console.log(' ⚠️ webpack.dev.js already appears to have commenting system integration');
521
+ return;
522
+ }
523
+
524
+ // Webpack middleware template (inline since we don't have a separate template file)
525
+ // Note: This middleware uses native fetch() which requires Node.js 18+
526
+ const middlewareCode = `
527
+ // Load env vars for local OAuth/token exchange without bundling secrets into the client.
528
+ // Note: Requires Node.js 18+ for native fetch() support
529
+ try {
530
+ const dotenv = require('dotenv');
531
+ dotenv.config({ path: path.resolve(__dirname, '.env') });
532
+ dotenv.config({ path: path.resolve(__dirname, '.env.server'), override: true });
533
+ } catch (e) {
534
+ // no-op
535
+ }
536
+
537
+ const express = require('express');
538
+ devServer.app.use(express.json());
539
+
540
+ // GitHub OAuth Callback
541
+ devServer.app.get('/api/github-oauth-callback', async (req, res) => {
542
+ try {
543
+ const code = req.query.code;
544
+ if (!code) {
545
+ return res.status(400).send('Missing ?code from GitHub OAuth callback.');
546
+ }
547
+
548
+ const clientId = process.env.VITE_GITHUB_CLIENT_ID;
549
+ const clientSecret = process.env.GITHUB_CLIENT_SECRET;
550
+
551
+ if (!clientId || !clientSecret) {
552
+ return res.status(500).send('Missing GitHub OAuth credentials.');
553
+ }
554
+
555
+ const tokenResp = await fetch('https://github.com/login/oauth/access_token', {
556
+ method: 'POST',
557
+ headers: {
558
+ 'Accept': 'application/json',
559
+ 'Content-Type': 'application/json',
560
+ },
561
+ body: JSON.stringify({
562
+ client_id: clientId,
563
+ client_secret: clientSecret,
564
+ code,
565
+ }),
566
+ });
567
+
568
+ const tokenData = await tokenResp.json();
569
+ if (!tokenResp.ok || tokenData.error) {
570
+ return res.status(500).send(\`OAuth token exchange failed: \${tokenData.error || tokenResp.statusText}\`);
571
+ }
572
+
573
+ const accessToken = tokenData.access_token;
574
+ if (!accessToken) {
575
+ return res.status(500).send('OAuth token exchange did not return an access_token.');
576
+ }
577
+
578
+ const userResp = await fetch('https://api.github.com/user', {
579
+ headers: {
580
+ 'Accept': 'application/vnd.github+json',
581
+ 'Authorization': \`token \${accessToken}\`,
582
+ 'User-Agent': 'hale-commenting-system',
583
+ },
584
+ });
585
+ const user = await userResp.json();
586
+ if (!userResp.ok) {
587
+ return res.status(500).send(\`Failed to fetch GitHub user: \${user.message || userResp.statusText}\`);
588
+ }
589
+
590
+ const login = encodeURIComponent(user.login || '');
591
+ const avatar = encodeURIComponent(user.avatar_url || '');
592
+ const token = encodeURIComponent(accessToken);
593
+
594
+ return res.redirect(\`/#/auth-callback?token=\${token}&login=\${login}&avatar=\${avatar}\`);
595
+ } catch (err) {
596
+ console.error(err);
597
+ return res.status(500).send('Unhandled OAuth callback error. See dev server logs.');
598
+ }
599
+ });
600
+
601
+ // GitHub API Proxy
602
+ devServer.app.post('/api/github-api', async (req, res) => {
603
+ try {
604
+ const { token, method, endpoint, data } = req.body || {};
605
+ if (!token) return res.status(401).json({ message: 'Missing token' });
606
+ if (!method || !endpoint) return res.status(400).json({ message: 'Missing method or endpoint' });
607
+
608
+ const url = \`https://api.github.com\${endpoint}\`;
609
+ const resp = await fetch(url, {
610
+ method,
611
+ headers: {
612
+ 'Accept': 'application/vnd.github+json',
613
+ 'Authorization': \`token \${token}\`,
614
+ 'User-Agent': 'hale-commenting-system',
615
+ ...(data ? { 'Content-Type': 'application/json' } : {}),
616
+ },
617
+ body: data ? JSON.stringify(data) : undefined,
618
+ });
619
+
620
+ const text = await resp.text();
621
+ const maybeJson = (() => {
622
+ try {
623
+ return JSON.parse(text);
624
+ } catch {
625
+ return text;
626
+ }
627
+ })();
628
+
629
+ return res.status(resp.status).json(maybeJson);
630
+ } catch (err) {
631
+ console.error(err);
632
+ return res.status(500).json({ message: 'Unhandled github-api proxy error. See dev server logs.' });
633
+ }
634
+ });
635
+
636
+ // Jira Issue Proxy
637
+ devServer.app.get('/api/jira-issue', async (req, res) => {
638
+ try {
639
+ const key = String(req.query.key || '').trim();
640
+ if (!key) return res.status(400).json({ message: 'Missing ?key (e.g. ABC-123)' });
641
+
642
+ const baseUrl = (process.env.VITE_JIRA_BASE_URL || 'https://issues.redhat.com').replace(/\\/+$/, '');
643
+ const email = process.env.JIRA_EMAIL;
644
+ const token = process.env.JIRA_API_TOKEN;
645
+
646
+ if (!token) {
647
+ return res.status(500).json({
648
+ message: 'Missing JIRA_API_TOKEN. For local dev, put it in .env.server (gitignored).',
649
+ });
650
+ }
651
+
652
+ const authHeader = email
653
+ ? \`Basic \${Buffer.from(\`\${email}:\${token}\`).toString('base64')}\`
654
+ : \`Bearer \${token}\`;
655
+
656
+ const buildUrl = (apiVersion) =>
657
+ \`\${baseUrl}/rest/api/\${apiVersion}/issue/\${encodeURIComponent(key)}?fields=summary,status,assignee,issuetype,priority,created,updated,description&expand=renderedFields\`;
658
+
659
+ const commonHeaders = {
660
+ 'Accept': 'application/json',
661
+ 'Authorization': authHeader,
662
+ 'User-Agent': 'hale-commenting-system',
663
+ };
664
+
665
+ const fetchOnce = async (apiVersion) => {
666
+ const r = await fetch(buildUrl(apiVersion), { headers: commonHeaders, redirect: 'manual' });
667
+ const text = await r.text();
668
+ const contentType = String(r.headers.get('content-type') || '');
669
+ const looksLikeHtml =
670
+ contentType.includes('text/html') ||
671
+ String(text || '').trim().startsWith('<');
672
+ return { r, text, contentType, looksLikeHtml };
673
+ };
674
+
675
+ const preferV2 = baseUrl.includes('issues.redhat.com');
676
+ const firstVersion = preferV2 ? '2' : '3';
677
+ const secondVersion = preferV2 ? '3' : '2';
678
+
679
+ let attempt = await fetchOnce(firstVersion);
680
+ if (
681
+ attempt.r.status === 404 ||
682
+ attempt.r.status === 302 ||
683
+ attempt.looksLikeHtml ||
684
+ attempt.r.status === 401 ||
685
+ attempt.r.status === 403
686
+ ) {
687
+ const fallback = await fetchOnce(secondVersion);
688
+ if (fallback.r.ok || attempt.looksLikeHtml || attempt.r.status === 302) {
689
+ attempt = fallback;
690
+ }
691
+ }
692
+
693
+ const resp = attempt.r;
694
+ const payloadText = attempt.text;
695
+ const contentType = attempt.contentType;
696
+
697
+ const payload = (() => {
698
+ try {
699
+ return JSON.parse(payloadText);
700
+ } catch {
701
+ return { message: payloadText };
702
+ }
703
+ })();
704
+
705
+ if (!resp.ok) {
706
+ const looksLikeHtml =
707
+ contentType.includes('text/html') ||
708
+ String(payloadText || '').trim().startsWith('<');
709
+
710
+ if (looksLikeHtml) {
711
+ return res.status(resp.status).json({
712
+ message:
713
+ resp.status === 401 || resp.status === 403
714
+ ? 'Unauthorized to Jira. Your token/auth scheme may be incorrect for this Jira instance.'
715
+ : \`Jira request failed (\${resp.status}).\`,
716
+ hint: email
717
+ ? 'You are using Basic auth (JIRA_EMAIL + JIRA_API_TOKEN). If this Jira uses PAT/Bearer tokens, remove JIRA_EMAIL and set only JIRA_API_TOKEN.'
718
+ : baseUrl.includes('issues.redhat.com')
719
+ ? 'You are using Bearer auth (JIRA_API_TOKEN). For issues.redhat.com, ensure you are using a PAT that works with REST API v2 and that JIRA_EMAIL is NOT set.'
720
+ : 'You are using Bearer auth (JIRA_API_TOKEN). If this Jira uses Jira Cloud API tokens, set JIRA_EMAIL as well.',
721
+ });
722
+ }
723
+
724
+ return res.status(resp.status).json({
725
+ message: payload?.message || \`Jira request failed (\${resp.status}).\`,
726
+ });
727
+ }
728
+
729
+ const issue = payload;
730
+ const fields = issue.fields || {};
731
+ const renderedFields = issue.renderedFields || {};
732
+
733
+ return res.json({
734
+ key: issue.key,
735
+ url: \`\${baseUrl}/browse/\${issue.key}\`,
736
+ summary: fields.summary || '',
737
+ status: fields.status?.name || '',
738
+ assignee: fields.assignee?.displayName || '',
739
+ issueType: fields.issuetype?.name || '',
740
+ priority: fields.priority?.name || '',
741
+ created: fields.created || '',
742
+ updated: fields.updated || '',
743
+ description: renderedFields.description || fields.description || '',
744
+ });
745
+ } catch (err) {
746
+ console.error(err);
747
+ return res.status(500).json({ message: 'Unhandled jira-issue proxy error. See dev server logs.' });
748
+ }
749
+ });
750
+ `;
751
+
752
+ // Find the setupMiddlewares function and inject our code
753
+ const setupMiddlewaresRegex = /(setupMiddlewares\s*:\s*\([^)]+\)\s*=>\s*\{)/;
754
+ const match = webpackContent.match(setupMiddlewaresRegex);
755
+
756
+ if (!match) {
757
+ console.log(' ⚠️ Could not find setupMiddlewares in webpack.dev.js');
758
+ console.log(' 📋 Manual integration required. See webpack middleware documentation\n');
759
+ return;
760
+ }
761
+
762
+ // Find where to inject (after express.json() setup, before return middlewares)
763
+ const expressJsonMatch = webpackContent.match(/devServer\.app\.use\(express\.json\(\)\);/);
764
+
765
+ if (expressJsonMatch) {
766
+ // Inject after express.json()
767
+ const insertIndex = expressJsonMatch.index + expressJsonMatch[0].length;
768
+ const before = webpackContent.substring(0, insertIndex);
769
+ const after = webpackContent.substring(insertIndex);
770
+
771
+ webpackContent = before + middlewareCode + '\n' + after;
772
+ fs.writeFileSync(webpackDevPath, webpackContent);
773
+ console.log(' ✅ Updated webpack.dev.js with server middleware');
774
+ } else {
775
+ // Try to inject at the beginning of setupMiddlewares
776
+ const insertIndex = match.index + match[0].length;
777
+ const before = webpackContent.substring(0, insertIndex);
778
+ const after = webpackContent.substring(insertIndex);
779
+
780
+ // Add dotenv loading and express setup if not present
781
+ let fullMiddlewareCode = middlewareCode;
782
+
783
+ // Check if dotenv is already loaded
784
+ if (!webpackContent.includes('dotenv.config')) {
785
+ fullMiddlewareCode = `// Load env vars for local OAuth/token exchange
786
+ try {
787
+ const dotenv = require('dotenv');
788
+ dotenv.config({ path: path.resolve(__dirname, '.env') });
789
+ dotenv.config({ path: path.resolve(__dirname, '.env.server'), override: true });
790
+ } catch (e) {
791
+ // no-op
792
+ }
793
+
794
+ const express = require('express');
795
+ devServer.app.use(express.json());
796
+
797
+ ` + middlewareCode;
798
+ } else if (!webpackContent.includes('express.json()')) {
799
+ fullMiddlewareCode = `const express = require('express');
800
+ devServer.app.use(express.json());
801
+
802
+ ` + middlewareCode;
803
+ }
804
+
805
+ webpackContent = before + '\n' + fullMiddlewareCode + '\n' + after;
806
+ fs.writeFileSync(webpackDevPath, webpackContent);
807
+ console.log(' ✅ Updated webpack.dev.js with server middleware');
808
+ }
809
+ }
810
+
58
811
  function getPackageVersion() {
59
812
  try {
60
813
  // Get the script's directory and find package.json relative to it
@@ -103,7 +856,7 @@ function modifyIndexTsx(filePath) {
103
856
  traverse(ast, {
104
857
  ImportDeclaration(path) {
105
858
  const source = path.node.source.value;
106
- if (source.includes('commenting-system') || source.includes('@app/commenting-system')) {
859
+ if (source.includes('commenting-system') || source.includes('@app/commenting-system') || source.includes('hale-commenting-system')) {
107
860
  hasCommentImport = true;
108
861
  // Check if providers are imported
109
862
  path.node.specifiers.forEach(spec => {
@@ -140,7 +893,7 @@ function modifyIndexTsx(filePath) {
140
893
  types.importSpecifier(types.identifier('CommentProvider'), types.identifier('CommentProvider')),
141
894
  types.importSpecifier(types.identifier('GitHubAuthProvider'), types.identifier('GitHubAuthProvider'))
142
895
  ],
143
- types.stringLiteral('@app/commenting-system')
896
+ types.stringLiteral('hale-commenting-system')
144
897
  );
145
898
 
146
899
  ast.program.body.splice(importIndex, 0, providerImports);
@@ -149,7 +902,7 @@ function modifyIndexTsx(filePath) {
149
902
  traverse(ast, {
150
903
  ImportDeclaration(path) {
151
904
  const source = path.node.source.value;
152
- if (source.includes('commenting-system') || source.includes('@app/commenting-system')) {
905
+ if (source.includes('commenting-system') || source.includes('@app/commenting-system') || source.includes('hale-commenting-system')) {
153
906
  const specifiers = path.node.specifiers || [];
154
907
  if (!hasCommentProvider) {
155
908
  specifiers.push(types.importSpecifier(types.identifier('CommentProvider'), types.identifier('CommentProvider')));
@@ -274,7 +1027,7 @@ function modifyAppLayoutTsx(filePath) {
274
1027
  traverse(ast, {
275
1028
  ImportDeclaration(path) {
276
1029
  const source = path.node.source.value;
277
- if (source.includes('commenting-system') || source.includes('@app/commenting-system')) {
1030
+ if (source.includes('commenting-system') || source.includes('@app/commenting-system') || source.includes('hale-commenting-system')) {
278
1031
  hasCommentImport = true;
279
1032
  // Check if components are imported
280
1033
  const specifiers = path.node.specifiers || [];
@@ -318,7 +1071,7 @@ function modifyAppLayoutTsx(filePath) {
318
1071
  types.importSpecifier(types.identifier('CommentPanel'), types.identifier('CommentPanel')),
319
1072
  types.importSpecifier(types.identifier('CommentOverlay'), types.identifier('CommentOverlay'))
320
1073
  ],
321
- types.stringLiteral('@app/commenting-system')
1074
+ types.stringLiteral('hale-commenting-system')
322
1075
  );
323
1076
 
324
1077
  ast.program.body.splice(importIndex, 0, componentImports);
@@ -377,21 +1130,372 @@ async function main() {
377
1130
  console.log('║' + ' '.repeat(padding) + title + ' '.repeat(borderLength - titleLength - padding - 2) + '║');
378
1131
  console.log('╚' + '═'.repeat(borderLength - 2) + '╝\n');
379
1132
 
380
- console.log('We\'ll automatically integrate the commenting system into your project.\n');
381
- console.log('This will modify the following files:');
382
- console.log(' src/app/index.tsx');
383
- console.log(' • src/app/routes.tsx');
384
- console.log(' • src/app/AppLayout/AppLayout.tsx\n');
1133
+ // Welcome & Explanation
1134
+ console.log('🚀 Welcome to Hale Commenting System!\n');
1135
+ console.log('This commenting system allows you to:');
1136
+ console.log(' • Add comments directly on your design pages');
1137
+ console.log(' • Sync comments with GitHub Issues');
1138
+ console.log(' • Link Jira tickets to pages');
1139
+ console.log(' • Store design goals and context\n');
1140
+
1141
+ console.log('Why GitHub?');
1142
+ console.log(' We use GitHub Issues to store and sync all comments. When you add a comment');
1143
+ console.log(' on a page, it creates a GitHub Issue. This allows comments to persist, sync');
1144
+ console.log(' across devices, and be managed like any other GitHub Issue.\n');
1145
+
1146
+ console.log('Why Jira?');
1147
+ console.log(' You can link Jira tickets to specific pages or sections. This helps connect');
1148
+ console.log(' design work to development tracking and provides context for reviewers.\n');
385
1149
 
386
- const answer = await question('Continue? (Y/n) ');
1150
+ // Step 1: Project Status Check
1151
+ console.log('📋 Step 1: Project Setup Check\n');
1152
+
1153
+ const hasProject = await prompt([
1154
+ {
1155
+ type: 'list',
1156
+ name: 'hasProject',
1157
+ message: 'Do you have a PatternFly Seed project set up locally?',
1158
+ choices: [
1159
+ { name: 'Yes, I have it set up', value: 'yes' },
1160
+ { name: 'No, I need help setting it up', value: 'no' }
1161
+ ]
1162
+ }
1163
+ ]);
387
1164
 
388
- if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== '' && answer.toLowerCase() !== 'yes') {
389
- console.log('\n Integration cancelled.');
1165
+ if (hasProject.hasProject === 'no') {
1166
+ console.log('\n📚 Setting up PatternFly Seed:\n');
1167
+ console.log('1. Fork the PatternFly Seed repository:');
1168
+ console.log(' Visit: https://github.com/patternfly/patternfly-react-seed');
1169
+ console.log(' Click "Fork" in the top right\n');
1170
+ console.log('2. Clone your fork locally:');
1171
+ console.log(' git clone https://github.com/YOUR_USERNAME/patternfly-react-seed.git');
1172
+ console.log(' cd patternfly-react-seed\n');
1173
+ console.log('3. Install dependencies:');
1174
+ console.log(' npm install\n');
1175
+ console.log('4. Run this setup again:');
1176
+ console.log(' npx hale-commenting-system init\n');
390
1177
  rl.close();
391
1178
  return;
392
1179
  }
393
1180
 
394
- console.log('\n✓ Scanning project structure...');
1181
+ // Check if it's actually a PF Seed project
1182
+ if (!detectPatternFlySeed()) {
1183
+ console.error('❌ Error: This doesn\'t appear to be a PatternFly Seed project.');
1184
+ console.error('Please run this command from a PatternFly Seed project directory.');
1185
+ rl.close();
1186
+ process.exit(1);
1187
+ }
1188
+
1189
+ // Detect project setup type
1190
+ const gitInfo = detectGitRemote();
1191
+ const setupType = detectProjectSetup();
1192
+
1193
+ let projectSetup = 'unknown';
1194
+ let owner = gitInfo?.owner;
1195
+ let repo = gitInfo?.repo;
1196
+
1197
+ if (setupType === 'none' || !gitInfo) {
1198
+ // No git remote - need to set up
1199
+ const setupAnswer = await prompt([
1200
+ {
1201
+ type: 'list',
1202
+ name: 'setupType',
1203
+ message: 'How did you set up your PatternFly Seed project?',
1204
+ choices: [
1205
+ { name: 'I forked the PatternFly Seed repo on GitHub', value: 'forked' },
1206
+ { name: 'I cloned the PatternFly Seed repo locally', value: 'cloned' },
1207
+ { name: 'I\'m not sure', value: 'unknown' }
1208
+ ]
1209
+ }
1210
+ ]);
1211
+ projectSetup = setupAnswer.setupType;
1212
+ } else {
1213
+ projectSetup = setupType;
1214
+ }
1215
+
1216
+ // Handle different setup types
1217
+ if (projectSetup === 'forked') {
1218
+ // Ask for owner/repo if not detected
1219
+ if (!owner || !repo) {
1220
+ const forkAnswers = await prompt([
1221
+ {
1222
+ type: 'input',
1223
+ name: 'owner',
1224
+ message: 'What is your GitHub username or organization name?',
1225
+ default: owner,
1226
+ validate: (input) => {
1227
+ if (!input.trim()) return 'Owner is required';
1228
+ return true;
1229
+ }
1230
+ },
1231
+ {
1232
+ type: 'input',
1233
+ name: 'repo',
1234
+ message: 'What is the name of your forked repository?',
1235
+ default: repo,
1236
+ validate: (input) => {
1237
+ if (!input.trim()) return 'Repository name is required';
1238
+ return true;
1239
+ }
1240
+ }
1241
+ ]);
1242
+ owner = forkAnswers.owner;
1243
+ repo = forkAnswers.repo;
1244
+ } else {
1245
+ console.log(`\n✅ Detected repository: ${owner}/${repo}\n`);
1246
+ }
1247
+ } else if (projectSetup === 'cloned') {
1248
+ console.log('\n📝 Since you cloned the repo, you\'ll need to create your own GitHub repository.\n');
1249
+ console.log('Steps:');
1250
+ console.log('1. Create a new repository on GitHub');
1251
+ console.log('2. Add it as a remote: git remote add origin <your-repo-url>');
1252
+ console.log('3. Push your code: git push -u origin main\n');
1253
+
1254
+ const hasCreated = await prompt([
1255
+ {
1256
+ type: 'confirm',
1257
+ name: 'created',
1258
+ message: 'Have you created and pushed to your GitHub repository?',
1259
+ default: false
1260
+ }
1261
+ ]);
1262
+
1263
+ if (!hasCreated.created) {
1264
+ console.log('\nPlease complete the steps above and run this setup again.');
1265
+ rl.close();
1266
+ return;
1267
+ }
1268
+
1269
+ // Ask for owner/repo
1270
+ const repoAnswers = await prompt([
1271
+ {
1272
+ type: 'input',
1273
+ name: 'owner',
1274
+ message: 'What is your GitHub username or organization name?',
1275
+ validate: (input) => {
1276
+ if (!input.trim()) return 'Owner is required';
1277
+ return true;
1278
+ }
1279
+ },
1280
+ {
1281
+ type: 'input',
1282
+ name: 'repo',
1283
+ message: 'What is the name of your GitHub repository?',
1284
+ validate: (input) => {
1285
+ if (!input.trim()) return 'Repository name is required';
1286
+ return true;
1287
+ }
1288
+ }
1289
+ ]);
1290
+ owner = repoAnswers.owner;
1291
+ repo = repoAnswers.repo;
1292
+ } else if (projectSetup === 'unknown') {
1293
+ // Try to detect from git
1294
+ if (gitInfo && gitInfo.owner && gitInfo.repo) {
1295
+ console.log(`\n✅ Detected repository: ${gitInfo.owner}/${gitInfo.repo}\n`);
1296
+ owner = gitInfo.owner;
1297
+ repo = gitInfo.repo;
1298
+ } else {
1299
+ // Ask for owner/repo
1300
+ const repoAnswers = await prompt([
1301
+ {
1302
+ type: 'input',
1303
+ name: 'owner',
1304
+ message: 'What is your GitHub username or organization name?',
1305
+ validate: (input) => {
1306
+ if (!input.trim()) return 'Owner is required';
1307
+ return true;
1308
+ }
1309
+ },
1310
+ {
1311
+ type: 'input',
1312
+ name: 'repo',
1313
+ message: 'What is the name of your GitHub repository?',
1314
+ validate: (input) => {
1315
+ if (!input.trim()) return 'Repository name is required';
1316
+ return true;
1317
+ }
1318
+ }
1319
+ ]);
1320
+ owner = repoAnswers.owner;
1321
+ repo = repoAnswers.repo;
1322
+ }
1323
+ }
1324
+
1325
+ // Step 2: GitHub OAuth Setup (Optional)
1326
+ console.log('\n📦 Step 2: GitHub Integration (Optional)\n');
1327
+ console.log('GitHub integration allows comments to sync with GitHub Issues.');
1328
+ console.log('You can set this up now or add it later.\n');
1329
+
1330
+ const setupGitHub = await prompt([
1331
+ {
1332
+ type: 'confirm',
1333
+ name: 'setup',
1334
+ message: 'Do you want to set up GitHub integration now?',
1335
+ default: true
1336
+ }
1337
+ ]);
1338
+
1339
+ let githubConfig = null;
1340
+ let githubValid = false;
1341
+
1342
+ if (setupGitHub.setup) {
1343
+ console.log('\nTo sync comments with GitHub Issues, we need to authenticate with GitHub.');
1344
+ console.log('This requires creating a GitHub OAuth App.\n');
1345
+ console.log('Instructions:');
1346
+ console.log('1. Visit: https://github.com/settings/developers');
1347
+ console.log('2. Click "New OAuth App"');
1348
+ console.log('3. Fill in the form:');
1349
+ console.log(' - Application name: Your app name (e.g., "My Design Comments")');
1350
+ console.log(' - Homepage URL: http://localhost:9000 (or your dev server URL)');
1351
+ console.log(' - Authorization callback URL: http://localhost:9000/api/github-oauth-callback');
1352
+ console.log('4. Click "Register application"');
1353
+ console.log('5. Copy the Client ID and generate a Client Secret\n');
1354
+
1355
+ const githubAnswers = await prompt([
1356
+ {
1357
+ type: 'input',
1358
+ name: 'clientId',
1359
+ message: 'GitHub OAuth Client ID:',
1360
+ validate: (input) => {
1361
+ if (!input.trim()) return 'Client ID is required';
1362
+ return true;
1363
+ }
1364
+ },
1365
+ {
1366
+ type: 'password',
1367
+ name: 'clientSecret',
1368
+ message: 'GitHub OAuth Client Secret:',
1369
+ mask: '*',
1370
+ validate: (input) => {
1371
+ if (!input.trim()) return 'Client Secret is required';
1372
+ return true;
1373
+ }
1374
+ }
1375
+ ]);
1376
+
1377
+ // Validate GitHub credentials
1378
+ console.log('\n🔍 Validating GitHub credentials...');
1379
+ githubValid = await validateGitHubCredentials(
1380
+ githubAnswers.clientId,
1381
+ githubAnswers.clientSecret,
1382
+ owner,
1383
+ repo
1384
+ );
1385
+
1386
+ if (!githubValid) {
1387
+ console.error('❌ GitHub validation failed. Please check your credentials and try again.');
1388
+ rl.close();
1389
+ process.exit(1);
1390
+ }
1391
+ console.log('✅ GitHub credentials validated!\n');
1392
+
1393
+ githubConfig = {
1394
+ clientId: githubAnswers.clientId,
1395
+ clientSecret: githubAnswers.clientSecret,
1396
+ owner: owner,
1397
+ repo: repo
1398
+ };
1399
+ } else {
1400
+ console.log('\n⏭️ Skipping GitHub setup. You can add it later by editing .env and .env.server files.\n');
1401
+ }
1402
+
1403
+ // Step 3: Jira Setup (Optional)
1404
+ console.log('🎫 Step 3: Jira Integration (Optional)\n');
1405
+ console.log('Jira integration allows you to link Jira tickets to pages in your design.');
1406
+ console.log('You can set this up now or add it later.\n');
1407
+
1408
+ const setupJira = await prompt([
1409
+ {
1410
+ type: 'confirm',
1411
+ name: 'setup',
1412
+ message: 'Do you want to set up Jira integration now?',
1413
+ default: true
1414
+ }
1415
+ ]);
1416
+
1417
+ let jiraConfig = null;
1418
+ let jiraValid = false;
1419
+
1420
+ if (setupJira.setup) {
1421
+ console.log('\nFor Red Hat Jira, generate a Personal Access Token:');
1422
+ console.log('1. Visit: https://issues.redhat.com/secure/ViewProfile.jspa');
1423
+ console.log('2. Click "Personal Access Tokens" in the left sidebar');
1424
+ console.log('3. Click "Create token"');
1425
+ console.log('4. Give it a name (e.g., "Hale Commenting System")');
1426
+ console.log('5. Remove expiration (or set a long expiration)');
1427
+ console.log('6. Click "Create" and copy the token\n');
1428
+ console.log('Note: We use Bearer token authentication (no email required for Red Hat Jira).\n');
1429
+
1430
+ const jiraAnswers = await prompt([
1431
+ {
1432
+ type: 'input',
1433
+ name: 'baseUrl',
1434
+ message: 'Jira Base URL (press Enter for Red Hat Jira):',
1435
+ default: 'https://issues.redhat.com',
1436
+ validate: (input) => {
1437
+ if (!input.trim()) return 'Base URL is required';
1438
+ try {
1439
+ new URL(input);
1440
+ return true;
1441
+ } catch {
1442
+ return 'Please enter a valid URL';
1443
+ }
1444
+ }
1445
+ },
1446
+ {
1447
+ type: 'password',
1448
+ name: 'apiToken',
1449
+ message: 'Jira API Token:',
1450
+ mask: '*',
1451
+ validate: (input) => {
1452
+ if (!input.trim()) return 'API Token is required';
1453
+ return true;
1454
+ }
1455
+ }
1456
+ ]);
1457
+
1458
+ // Validate Jira credentials
1459
+ console.log('\n🔍 Validating Jira credentials...');
1460
+ jiraValid = await validateJiraCredentials(
1461
+ jiraAnswers.baseUrl,
1462
+ jiraAnswers.apiToken,
1463
+ undefined // No email for Bearer token
1464
+ );
1465
+
1466
+ if (!jiraValid) {
1467
+ console.error('❌ Jira validation failed. Please check your credentials and try again.');
1468
+ rl.close();
1469
+ process.exit(1);
1470
+ }
1471
+ console.log('✅ Jira credentials validated!\n');
1472
+
1473
+ jiraConfig = {
1474
+ baseUrl: jiraAnswers.baseUrl,
1475
+ apiToken: jiraAnswers.apiToken,
1476
+ email: undefined
1477
+ };
1478
+ } else {
1479
+ console.log('\n⏭️ Skipping Jira setup. You can add it later by editing .env and .env.server files.\n');
1480
+ }
1481
+
1482
+ // Step 4: Generate files
1483
+ console.log('📝 Step 4: Generating configuration files...\n');
1484
+ generateFiles({
1485
+ github: githubConfig,
1486
+ jira: jiraConfig,
1487
+ owner: owner,
1488
+ repo: repo
1489
+ });
1490
+
1491
+ // Step 5: Integrate into project
1492
+ console.log('\n🔧 Step 5: Integrating into PatternFly Seed project...\n');
1493
+
1494
+ console.log('This will modify the following files:');
1495
+ console.log(' • src/app/index.tsx');
1496
+ console.log(' • src/app/routes.tsx');
1497
+ console.log(' • src/app/AppLayout/AppLayout.tsx');
1498
+ console.log(' • webpack.dev.js\n');
395
1499
 
396
1500
  const indexPath = findFile('index.tsx');
397
1501
  const routesPath = findFile('routes.tsx');
@@ -445,6 +1549,10 @@ async function main() {
445
1549
  skippedCount++;
446
1550
  }
447
1551
 
1552
+ // Integrate webpack middleware
1553
+ console.log('\n📝 webpack.dev.js');
1554
+ integrateWebpackMiddleware();
1555
+
448
1556
  console.log('\n╔══════════════════════════════════════════════════════════╗');
449
1557
  console.log('║ ✅ Integration Complete! ║');
450
1558
  console.log('╚══════════════════════════════════════════════════════════╝\n');
@@ -457,9 +1565,20 @@ async function main() {
457
1565
  }
458
1566
 
459
1567
  console.log('\nNext steps:');
460
- console.log(' 1. Restart your dev server: npm run start:dev');
461
- console.log(' 2. The commenting system will be active automatically');
462
- console.log(' 3. Click anywhere on the page to create comments\n');
1568
+ console.log('1. Start your dev server: npm run start:dev');
1569
+ console.log(' (If it\'s already running, restart it to load the new configuration)');
1570
+ console.log('2. The commenting system will be available in your app!\n');
1571
+
1572
+ if (!githubConfig || !jiraConfig) {
1573
+ console.log('📝 To add integrations later:');
1574
+ if (!githubConfig) {
1575
+ console.log(' • GitHub: Edit .env and .env.server files (see comments in files for instructions)');
1576
+ }
1577
+ if (!jiraConfig) {
1578
+ console.log(' • Jira: Edit .env and .env.server files (see comments in files for instructions)');
1579
+ }
1580
+ console.log(' • Then restart your dev server\n');
1581
+ }
463
1582
 
464
1583
  rl.close();
465
1584
  }