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.
- package/package.json +9 -5
- 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
|
+
"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
|
},
|
package/scripts/integrate.js
CHANGED
|
@@ -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('
|
|
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('
|
|
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
|
-
|
|
381
|
-
console.log('
|
|
382
|
-
console.log('
|
|
383
|
-
console.log(' •
|
|
384
|
-
console.log(' •
|
|
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
|
-
|
|
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 (
|
|
389
|
-
console.log('\n
|
|
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
|
-
|
|
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('
|
|
461
|
-
console.log('
|
|
462
|
-
console.log('
|
|
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
|
}
|