hale-commenting-system 2.2.2 → 2.2.4

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