hearback 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hearback.js +171 -14
- package/package.json +1 -1
package/bin/hearback.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
6
|
+
import { execSync, spawn } from 'node:child_process';
|
|
7
|
+
import { platform } from 'node:os';
|
|
7
8
|
|
|
8
9
|
const RESET = '\x1b[0m';
|
|
9
10
|
const BOLD = '\x1b[1m';
|
|
@@ -18,13 +19,50 @@ function done(msg) { log(` ${GREEN}✓${RESET} ${msg}`); }
|
|
|
18
19
|
function info(msg) { log(` ${DIM}${msg}${RESET}`); }
|
|
19
20
|
function warn(msg) { log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
20
21
|
|
|
22
|
+
// Readline with line-buffering — works with both interactive TTY and piped
|
|
23
|
+
// input. `rl.question()` has a bug on piped stdin where only the first prompt
|
|
24
|
+
// returns; using `rl.on('line')` + a queue avoids that.
|
|
25
|
+
let _rl = null;
|
|
26
|
+
const _pendingLines = [];
|
|
27
|
+
const _pendingAsks = [];
|
|
28
|
+
|
|
29
|
+
function getRl() {
|
|
30
|
+
if (!_rl) {
|
|
31
|
+
_rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY });
|
|
32
|
+
_rl.on('line', (line) => {
|
|
33
|
+
if (_pendingAsks.length > 0) {
|
|
34
|
+
const resolver = _pendingAsks.shift();
|
|
35
|
+
resolver(line.trim());
|
|
36
|
+
} else {
|
|
37
|
+
_pendingLines.push(line);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
_rl.on('close', () => {
|
|
41
|
+
// Flush any waiters with empty strings (avoids hanging forever)
|
|
42
|
+
while (_pendingAsks.length > 0) {
|
|
43
|
+
_pendingAsks.shift()('');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return _rl;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function closeRl() {
|
|
51
|
+
if (_rl) {
|
|
52
|
+
_rl.close();
|
|
53
|
+
_rl = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
async function ask(question) {
|
|
22
|
-
|
|
58
|
+
getRl();
|
|
59
|
+
process.stdout.write(` ${question} `);
|
|
23
60
|
return new Promise((resolve) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
61
|
+
if (_pendingLines.length > 0) {
|
|
62
|
+
resolve(_pendingLines.shift().trim());
|
|
63
|
+
} else {
|
|
64
|
+
_pendingAsks.push(resolve);
|
|
65
|
+
}
|
|
28
66
|
});
|
|
29
67
|
}
|
|
30
68
|
|
|
@@ -207,6 +245,53 @@ function injectNextjsWidget(content, endpoint) {
|
|
|
207
245
|
return result;
|
|
208
246
|
}
|
|
209
247
|
|
|
248
|
+
// --- Token helpers ---
|
|
249
|
+
|
|
250
|
+
/** Returns true if the `gh` CLI is installed and authenticated. */
|
|
251
|
+
function hasGhCli() {
|
|
252
|
+
try {
|
|
253
|
+
execSync('gh auth status', { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Read token from `gh auth token`. Returns null on failure. */
|
|
261
|
+
function getGhToken() {
|
|
262
|
+
try {
|
|
263
|
+
return execSync('gh auth token', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Open a URL in the user's default browser. Silent-fails if unavailable. */
|
|
270
|
+
function openBrowser(url) {
|
|
271
|
+
const cmd = platform() === 'darwin' ? 'open'
|
|
272
|
+
: platform() === 'win32' ? 'start'
|
|
273
|
+
: 'xdg-open';
|
|
274
|
+
try {
|
|
275
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
276
|
+
return true;
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Build a GitHub fine-grained token URL with as much pre-filled as possible.
|
|
284
|
+
* Sadly GitHub does NOT support pre-filling permissions via URL params, but
|
|
285
|
+
* we can pre-fill the name + description to save time.
|
|
286
|
+
*/
|
|
287
|
+
function buildTokenUrl(repoName) {
|
|
288
|
+
const params = new URLSearchParams({
|
|
289
|
+
name: `hearback-${repoName.split('/').pop()}`,
|
|
290
|
+
description: 'Token for hearback feedback widget',
|
|
291
|
+
});
|
|
292
|
+
return `https://github.com/settings/personal-access-tokens/new?${params}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
210
295
|
// --- Detect project type ---
|
|
211
296
|
|
|
212
297
|
function detectProject(cwd) {
|
|
@@ -352,7 +437,8 @@ ${BOLD}Usage:${RESET}
|
|
|
352
437
|
|
|
353
438
|
const pkgToInstall = project.type === 'agent' ? 'hearback-agent-skill' : 'hearback-server';
|
|
354
439
|
try {
|
|
355
|
-
|
|
440
|
+
// stdio: ['ignore', 'pipe', 'pipe'] — don't let npm consume our piped stdin
|
|
441
|
+
execSync(`npm install ${pkgToInstall}`, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
356
442
|
done(`${pkgToInstall} installed`);
|
|
357
443
|
} catch {
|
|
358
444
|
warn(`Could not install ${pkgToInstall} (not yet published to npm)`);
|
|
@@ -449,23 +535,88 @@ ${BOLD}Usage:${RESET}
|
|
|
449
535
|
}
|
|
450
536
|
}
|
|
451
537
|
|
|
452
|
-
//
|
|
538
|
+
// Step 5 (was 4.5): GitHub token — interactive, with multiple fast paths
|
|
539
|
+
step(5, 'GitHub token');
|
|
540
|
+
|
|
453
541
|
const envFile = existsSync(join(cwd, '.env.local')) ? '.env.local' : '.env';
|
|
454
542
|
const envPath = join(cwd, envFile);
|
|
455
543
|
const envContent = existsSync(envPath) ? readFileSync(envPath, 'utf-8') : '';
|
|
544
|
+
const hasExistingToken = /GITHUB_TOKEN=\S+/.test(envContent) || /FEEDBACK_GITHUB_TOKEN=\S+/.test(envContent);
|
|
456
545
|
|
|
546
|
+
let token = '';
|
|
547
|
+
|
|
548
|
+
if (hasExistingToken) {
|
|
549
|
+
info(`${envFile} already has a GITHUB_TOKEN — skipping token setup`);
|
|
550
|
+
} else {
|
|
551
|
+
info('hearback needs a GitHub token with Issues: read-write (and Contents: read-write for screenshots)');
|
|
552
|
+
log('');
|
|
553
|
+
log(` ${BOLD}How would you like to provide the token?${RESET}`);
|
|
554
|
+
log(` ${DIM}1)${RESET} Reuse my gh CLI token ${hasGhCli() ? GREEN + '(detected)' + RESET : DIM + '(gh CLI not found)' + RESET}`);
|
|
555
|
+
log(` ${DIM}2)${RESET} Generate a new fine-grained token on GitHub (opens browser)`);
|
|
556
|
+
log(` ${DIM}3)${RESET} I'll paste one manually`);
|
|
557
|
+
log(` ${DIM}4)${RESET} Skip — I'll add it to ${envFile} later`);
|
|
558
|
+
log('');
|
|
559
|
+
const choice = (await ask('Choose [1/2/3/4]:')).trim() || '1';
|
|
560
|
+
|
|
561
|
+
if (choice === '1') {
|
|
562
|
+
if (!hasGhCli()) {
|
|
563
|
+
warn('gh CLI not found or not authenticated. Install: https://cli.github.com/');
|
|
564
|
+
info('Falling back to option 2 (browser)');
|
|
565
|
+
} else {
|
|
566
|
+
const ghToken = getGhToken();
|
|
567
|
+
if (ghToken) {
|
|
568
|
+
token = ghToken;
|
|
569
|
+
done('Using gh CLI token');
|
|
570
|
+
warn('Note: gh CLI tokens have broad permissions. For stricter scoping, choose option 2 next time.');
|
|
571
|
+
} else {
|
|
572
|
+
warn('Could not read gh token');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!token && (choice === '2' || choice === '1')) {
|
|
578
|
+
const url = buildTokenUrl(repo);
|
|
579
|
+
info('Opening GitHub in your browser...');
|
|
580
|
+
info(`If it does not open, paste this URL manually:`);
|
|
581
|
+
log(` ${DIM}${url}${RESET}`);
|
|
582
|
+
log('');
|
|
583
|
+
info(`Set these permissions:`);
|
|
584
|
+
log(` ${DIM}- Repository access: Only select repositories → ${repo}${RESET}`);
|
|
585
|
+
log(` ${DIM}- Permissions → Issues: Read and write${RESET}`);
|
|
586
|
+
log(` ${DIM}- Permissions → Contents: Read and write (for screenshots)${RESET}`);
|
|
587
|
+
log('');
|
|
588
|
+
openBrowser(url);
|
|
589
|
+
token = (await ask('Paste your token (starts with github_pat_...):')).trim();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!token && choice === '3') {
|
|
593
|
+
token = (await ask('Paste your token:')).trim();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (choice === '4' || (!token && choice !== '1' && choice !== '2' && choice !== '3')) {
|
|
597
|
+
info(`Skipping. Add GITHUB_TOKEN to ${envFile} before running the dev server.`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Write env vars
|
|
457
602
|
const newVars = [];
|
|
458
603
|
if (!envContent.includes('FEEDBACK_REPO')) newVars.push(`FEEDBACK_REPO=${repo}`);
|
|
459
|
-
if (!envContent.includes('GITHUB_TOKEN'))
|
|
604
|
+
if (!envContent.includes('GITHUB_TOKEN')) {
|
|
605
|
+
if (token) {
|
|
606
|
+
newVars.push(`GITHUB_TOKEN=${token}`);
|
|
607
|
+
} else {
|
|
608
|
+
newVars.push(`GITHUB_TOKEN= # Generate at https://github.com/settings/personal-access-tokens/new (Issues: read/write, Contents: read/write)`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
460
611
|
|
|
461
612
|
if (newVars.length > 0) {
|
|
462
613
|
const addition = '\n# hearback\n' + newVars.join('\n') + '\n';
|
|
463
614
|
writeFileSync(envPath, envContent + addition);
|
|
464
|
-
done(`
|
|
615
|
+
done(token ? `Wrote FEEDBACK_REPO + GITHUB_TOKEN to ${envFile}` : `Wrote FEEDBACK_REPO to ${envFile}`);
|
|
465
616
|
}
|
|
466
617
|
|
|
467
|
-
// Step
|
|
468
|
-
step(
|
|
618
|
+
// Step 6: Notification workflow
|
|
619
|
+
step(6, 'Notification loop');
|
|
469
620
|
const wantNotify = await confirm('Set up email notifications when issues are fixed?');
|
|
470
621
|
|
|
471
622
|
if (wantNotify) {
|
|
@@ -477,10 +628,16 @@ ${BOLD}Usage:${RESET}
|
|
|
477
628
|
}
|
|
478
629
|
|
|
479
630
|
// Done
|
|
631
|
+
closeRl();
|
|
480
632
|
log(`\n${GREEN}${BOLD}✓ hearback is ready!${RESET}\n`);
|
|
481
633
|
log(`${DIM}Next steps:`);
|
|
482
|
-
|
|
483
|
-
|
|
634
|
+
if (!token && !hasExistingToken) {
|
|
635
|
+
log(` 1. Add your GITHUB_TOKEN to ${envFile}`);
|
|
636
|
+
log(` 2. Run your dev server and test the feedback button${RESET}\n`);
|
|
637
|
+
} else {
|
|
638
|
+
log(` 1. Run your dev server (e.g. npm run dev / bun dev)`);
|
|
639
|
+
log(` 2. Look for the feedback button in the bottom-right corner${RESET}\n`);
|
|
640
|
+
}
|
|
484
641
|
}
|
|
485
642
|
|
|
486
643
|
main().catch((err) => {
|
package/package.json
CHANGED