@stephenov/feedbackwidget 0.7.0 → 2.0.0

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/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js CHANGED
@@ -27,7 +27,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_http = __toESM(require("http"));
28
28
  var import_open = __toESM(require("open"));
29
29
  var import_crypto = require("crypto");
30
- var import_child_process = require("child_process");
31
30
  var import_fs = __toESM(require("fs"));
32
31
  var import_path = __toESM(require("path"));
33
32
  var API_BASE = "https://feedbackwidget-api.vercel.app";
@@ -44,7 +43,31 @@ async function main() {
44
43
  }
45
44
  }
46
45
  async function initProject() {
47
- console.log("\n\u{1F3A4} feedbackwidget - Voice-first feedback for your app\n");
46
+ console.log("\n\u{1F3A4} feedbackwidget - Voice-first feedback for Next.js\n");
47
+ const cwd = process.cwd();
48
+ console.log("Checking project...\n");
49
+ const layoutPath = findNextJsLayout(cwd);
50
+ if (!layoutPath) {
51
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
52
+ console.log("\u274C Next.js App Router not detected\n");
53
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
54
+ console.log("This package only works with Next.js App Router.\n");
55
+ console.log("Make sure you have an app/layout.tsx file.\n");
56
+ console.log("Learn more: https://nextjs.org/docs/app\n");
57
+ return;
58
+ }
59
+ console.log(`\u2713 Found: ${layoutPath.replace(cwd + "/", "")}
60
+ `);
61
+ const existingContent = import_fs.default.readFileSync(layoutPath, "utf-8");
62
+ if (existingContent.includes("FeedbackWidget")) {
63
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
64
+ console.log("\u2713 Widget already installed!\n");
65
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
66
+ console.log("Run: npm run dev\n");
67
+ console.log("Dashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
68
+ return;
69
+ }
70
+ console.log("Opening browser to sign in...\n");
48
71
  const state = (0, import_crypto.randomBytes)(16).toString("hex");
49
72
  const apiKey = await new Promise((resolve, reject) => {
50
73
  const server = import_http.default.createServer((req, res) => {
@@ -62,7 +85,7 @@ async function initProject() {
62
85
  }
63
86
  if (receivedState !== state) {
64
87
  res.writeHead(200, { "Content-Type": "text/html" });
65
- res.end(errorPage("Invalid state - possible CSRF attack"));
88
+ res.end(errorPage("Invalid state"));
66
89
  server.close();
67
90
  reject(new Error("Invalid state"));
68
91
  return;
@@ -77,7 +100,6 @@ async function initProject() {
77
100
  });
78
101
  server.listen(CLI_PORT, () => {
79
102
  const authUrl = `${API_BASE}/cli/auth?state=${state}&port=${CLI_PORT}`;
80
- console.log("Opening browser to authenticate...\n");
81
103
  console.log(` If browser doesn't open, visit:
82
104
  ${authUrl}
83
105
  `);
@@ -88,71 +110,59 @@ async function initProject() {
88
110
  reject(new Error("Authentication timed out"));
89
111
  }, 5 * 60 * 1e3);
90
112
  });
91
- const framework = detectFramework();
92
- const cwd = process.cwd();
93
- const fullPath = framework.fullPath ? import_path.default.join(cwd, framework.fullPath) : null;
94
- const targetFile = fullPath || framework.file;
95
- const isAstro = framework.name === "Astro";
113
+ console.log("\u2713 Authenticated\n");
114
+ console.log("Installing widget...\n");
115
+ let content = import_fs.default.readFileSync(layoutPath, "utf-8");
96
116
  const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
97
- const componentSnippet = isAstro ? `<FeedbackWidget client:load apiKey="${apiKey}" />` : `<FeedbackWidget apiKey="${apiKey}" />`;
98
- console.log("\n\u2705 Authenticated!\n");
99
- if (framework.name) {
100
- console.log(`Detected: ${framework.name}
101
- `);
117
+ const componentLine = `<FeedbackWidget apiKey="${apiKey}" />`;
118
+ const lines = content.split("\n");
119
+ let lastImportIndex = -1;
120
+ for (let i = 0; i < lines.length; i++) {
121
+ if (lines[i].startsWith("import ")) {
122
+ lastImportIndex = i;
123
+ }
102
124
  }
103
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
104
- console.log("STEP 1: Add the import\n");
105
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
106
- console.log(`Open: ${targetFile}
107
- `);
108
- if (isAstro) {
109
- console.log("Add this inside the --- frontmatter block:\n");
110
- } else {
111
- console.log("Add this with your other imports:\n");
125
+ if (lastImportIndex >= 0) {
126
+ lines.splice(lastImportIndex + 1, 0, importLine);
127
+ content = lines.join("\n");
112
128
  }
113
- console.log(` ${importLine}
114
- `);
115
- await waitForEnter("Press ENTER when you've added the import...");
116
- console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
117
- console.log("STEP 2: Add the component\n");
118
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
119
- console.log(`In the same file, add this ${framework.note || "inside <body>"}:
129
+ const bodyMatch = content.match(/<body[^>]*>/);
130
+ if (bodyMatch && bodyMatch.index !== void 0) {
131
+ const insertPos = bodyMatch.index + bodyMatch[0].length;
132
+ content = content.slice(0, insertPos) + "\n " + componentLine + content.slice(insertPos);
133
+ }
134
+ import_fs.default.writeFileSync(layoutPath, content, "utf-8");
135
+ const verify = import_fs.default.readFileSync(layoutPath, "utf-8");
136
+ if (!verify.includes("FeedbackWidget")) {
137
+ console.log("\u274C Failed to install. Please add manually:\n");
138
+ console.log(` ${importLine}
120
139
  `);
121
- console.log(` ${componentSnippet}
140
+ console.log(` ${componentLine}
122
141
  `);
123
- try {
124
- await copyToClipboard(componentSnippet);
125
- console.log(" \u{1F4CB} Copied to clipboard!\n");
126
- } catch {
127
- }
128
- await waitForEnter("Press ENTER when you've added the component...");
129
- let verified = false;
130
- if (fullPath && import_fs.default.existsSync(fullPath)) {
131
- const content = import_fs.default.readFileSync(fullPath, "utf-8");
132
- if (content.includes("FeedbackWidget")) {
133
- verified = true;
134
- }
135
- }
136
- if (verified) {
137
- console.log("\n\u2705 Found FeedbackWidget in your file!\n");
142
+ return;
138
143
  }
139
144
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
140
- console.log("\u{1F389} SETUP COMPLETE!\n");
145
+ console.log("\u{1F389} INSTALLED!\n");
141
146
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
142
- console.log(`Run: ${framework.devCommand || "npm run dev"}
143
- `);
144
- console.log("Refresh your browser - the widget will appear in the bottom-right.\n");
145
- console.log("View feedback at: https://feedbackwidget-api.vercel.app/dashboard\n");
147
+ console.log("Next steps:\n");
148
+ console.log(" 1. Run: npm run dev\n");
149
+ console.log(" 2. Refresh your browser\n");
150
+ console.log(" 3. Widget appears in bottom-right corner\n");
151
+ console.log("\nDashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
146
152
  }
147
- function waitForEnter(prompt) {
148
- return new Promise((resolve) => {
149
- process.stdout.write(prompt);
150
- process.stdin.setRawMode?.(false);
151
- process.stdin.resume();
152
- process.stdin.once("data", () => {
153
- resolve();
154
- });
155
- });
153
+ function findNextJsLayout(cwd) {
154
+ const paths = [
155
+ import_path.default.join(cwd, "app", "layout.tsx"),
156
+ import_path.default.join(cwd, "app", "layout.js"),
157
+ import_path.default.join(cwd, "src", "app", "layout.tsx"),
158
+ import_path.default.join(cwd, "src", "app", "layout.js")
159
+ ];
160
+ for (const p of paths) {
161
+ if (import_fs.default.existsSync(p)) {
162
+ return p;
163
+ }
164
+ }
165
+ return null;
156
166
  }
157
167
  async function whoami() {
158
168
  console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
@@ -217,90 +227,4 @@ function errorPage(error) {
217
227
  </body>
218
228
  </html>`;
219
229
  }
220
- function detectFramework() {
221
- const cwd = process.cwd();
222
- try {
223
- const pkgPath = import_path.default.join(cwd, "package.json");
224
- const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
225
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
226
- if (deps.astro) {
227
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "layouts", "Layout.astro"))) {
228
- return { name: "Astro", file: "src/layouts/Layout.astro", fullPath: "src/layouts/Layout.astro", note: "inside <body>", devCommand: "npm run dev" };
229
- }
230
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "layouts", "BaseLayout.astro"))) {
231
- return { name: "Astro", file: "src/layouts/BaseLayout.astro", fullPath: "src/layouts/BaseLayout.astro", note: "inside <body>", devCommand: "npm run dev" };
232
- }
233
- return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
234
- }
235
- if (deps.next) {
236
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.tsx"))) {
237
- return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
238
- }
239
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.js"))) {
240
- return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
241
- }
242
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "app", "layout.tsx"))) {
243
- return { name: "Next.js (App Router)", file: "src/app/layout.tsx", fullPath: "src/app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
244
- }
245
- if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.tsx"))) {
246
- return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
247
- }
248
- if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.js"))) {
249
- return { name: "Next.js (Pages Router)", file: "pages/_app.js", fullPath: "pages/_app.js", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
250
- }
251
- }
252
- if (deps["@remix-run/react"]) {
253
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "root.tsx"))) {
254
- return { name: "Remix", file: "app/root.tsx", fullPath: "app/root.tsx", note: "inside <body>", devCommand: "npm run dev" };
255
- }
256
- }
257
- if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
258
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
259
- return { name: "Vite + React", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm run dev" };
260
- }
261
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.jsx"))) {
262
- return { name: "Vite + React", file: "src/App.jsx", fullPath: "src/App.jsx", note: "at the end of your App component", devCommand: "npm run dev" };
263
- }
264
- }
265
- if (deps["react-scripts"]) {
266
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
267
- return { name: "Create React App", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm start" };
268
- }
269
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.js"))) {
270
- return { name: "Create React App", file: "src/App.js", fullPath: "src/App.js", note: "at the end of your App component", devCommand: "npm start" };
271
- }
272
- }
273
- if (deps.react) {
274
- return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
275
- }
276
- } catch {
277
- }
278
- return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
279
- }
280
- async function copyToClipboard(text) {
281
- return new Promise((resolve, reject) => {
282
- const platform = process.platform;
283
- let cmd;
284
- let args = [];
285
- if (platform === "darwin") {
286
- cmd = "pbcopy";
287
- } else if (platform === "linux") {
288
- cmd = "xclip";
289
- args = ["-selection", "clipboard"];
290
- } else if (platform === "win32") {
291
- cmd = "clip";
292
- } else {
293
- reject(new Error("Unsupported platform"));
294
- return;
295
- }
296
- const proc = (0, import_child_process.spawn)(cmd, args);
297
- proc.stdin.write(text);
298
- proc.stdin.end();
299
- proc.on("close", (code) => {
300
- if (code === 0) resolve();
301
- else reject(new Error(`Clipboard failed with code ${code}`));
302
- });
303
- proc.on("error", reject);
304
- });
305
- }
306
230
  main().catch(console.error);
package/dist/cli.mjs CHANGED
@@ -4,7 +4,6 @@
4
4
  import http from "http";
5
5
  import open from "open";
6
6
  import { randomBytes } from "crypto";
7
- import { spawn } from "child_process";
8
7
  import fs from "fs";
9
8
  import path from "path";
10
9
  var API_BASE = "https://feedbackwidget-api.vercel.app";
@@ -21,7 +20,31 @@ async function main() {
21
20
  }
22
21
  }
23
22
  async function initProject() {
24
- console.log("\n\u{1F3A4} feedbackwidget - Voice-first feedback for your app\n");
23
+ console.log("\n\u{1F3A4} feedbackwidget - Voice-first feedback for Next.js\n");
24
+ const cwd = process.cwd();
25
+ console.log("Checking project...\n");
26
+ const layoutPath = findNextJsLayout(cwd);
27
+ if (!layoutPath) {
28
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
29
+ console.log("\u274C Next.js App Router not detected\n");
30
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
31
+ console.log("This package only works with Next.js App Router.\n");
32
+ console.log("Make sure you have an app/layout.tsx file.\n");
33
+ console.log("Learn more: https://nextjs.org/docs/app\n");
34
+ return;
35
+ }
36
+ console.log(`\u2713 Found: ${layoutPath.replace(cwd + "/", "")}
37
+ `);
38
+ const existingContent = fs.readFileSync(layoutPath, "utf-8");
39
+ if (existingContent.includes("FeedbackWidget")) {
40
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
41
+ console.log("\u2713 Widget already installed!\n");
42
+ console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
43
+ console.log("Run: npm run dev\n");
44
+ console.log("Dashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
45
+ return;
46
+ }
47
+ console.log("Opening browser to sign in...\n");
25
48
  const state = randomBytes(16).toString("hex");
26
49
  const apiKey = await new Promise((resolve, reject) => {
27
50
  const server = http.createServer((req, res) => {
@@ -39,7 +62,7 @@ async function initProject() {
39
62
  }
40
63
  if (receivedState !== state) {
41
64
  res.writeHead(200, { "Content-Type": "text/html" });
42
- res.end(errorPage("Invalid state - possible CSRF attack"));
65
+ res.end(errorPage("Invalid state"));
43
66
  server.close();
44
67
  reject(new Error("Invalid state"));
45
68
  return;
@@ -54,7 +77,6 @@ async function initProject() {
54
77
  });
55
78
  server.listen(CLI_PORT, () => {
56
79
  const authUrl = `${API_BASE}/cli/auth?state=${state}&port=${CLI_PORT}`;
57
- console.log("Opening browser to authenticate...\n");
58
80
  console.log(` If browser doesn't open, visit:
59
81
  ${authUrl}
60
82
  `);
@@ -65,71 +87,59 @@ async function initProject() {
65
87
  reject(new Error("Authentication timed out"));
66
88
  }, 5 * 60 * 1e3);
67
89
  });
68
- const framework = detectFramework();
69
- const cwd = process.cwd();
70
- const fullPath = framework.fullPath ? path.join(cwd, framework.fullPath) : null;
71
- const targetFile = fullPath || framework.file;
72
- const isAstro = framework.name === "Astro";
90
+ console.log("\u2713 Authenticated\n");
91
+ console.log("Installing widget...\n");
92
+ let content = fs.readFileSync(layoutPath, "utf-8");
73
93
  const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
74
- const componentSnippet = isAstro ? `<FeedbackWidget client:load apiKey="${apiKey}" />` : `<FeedbackWidget apiKey="${apiKey}" />`;
75
- console.log("\n\u2705 Authenticated!\n");
76
- if (framework.name) {
77
- console.log(`Detected: ${framework.name}
78
- `);
94
+ const componentLine = `<FeedbackWidget apiKey="${apiKey}" />`;
95
+ const lines = content.split("\n");
96
+ let lastImportIndex = -1;
97
+ for (let i = 0; i < lines.length; i++) {
98
+ if (lines[i].startsWith("import ")) {
99
+ lastImportIndex = i;
100
+ }
79
101
  }
80
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
81
- console.log("STEP 1: Add the import\n");
82
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
83
- console.log(`Open: ${targetFile}
84
- `);
85
- if (isAstro) {
86
- console.log("Add this inside the --- frontmatter block:\n");
87
- } else {
88
- console.log("Add this with your other imports:\n");
102
+ if (lastImportIndex >= 0) {
103
+ lines.splice(lastImportIndex + 1, 0, importLine);
104
+ content = lines.join("\n");
89
105
  }
90
- console.log(` ${importLine}
91
- `);
92
- await waitForEnter("Press ENTER when you've added the import...");
93
- console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
94
- console.log("STEP 2: Add the component\n");
95
- console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
96
- console.log(`In the same file, add this ${framework.note || "inside <body>"}:
106
+ const bodyMatch = content.match(/<body[^>]*>/);
107
+ if (bodyMatch && bodyMatch.index !== void 0) {
108
+ const insertPos = bodyMatch.index + bodyMatch[0].length;
109
+ content = content.slice(0, insertPos) + "\n " + componentLine + content.slice(insertPos);
110
+ }
111
+ fs.writeFileSync(layoutPath, content, "utf-8");
112
+ const verify = fs.readFileSync(layoutPath, "utf-8");
113
+ if (!verify.includes("FeedbackWidget")) {
114
+ console.log("\u274C Failed to install. Please add manually:\n");
115
+ console.log(` ${importLine}
97
116
  `);
98
- console.log(` ${componentSnippet}
117
+ console.log(` ${componentLine}
99
118
  `);
100
- try {
101
- await copyToClipboard(componentSnippet);
102
- console.log(" \u{1F4CB} Copied to clipboard!\n");
103
- } catch {
104
- }
105
- await waitForEnter("Press ENTER when you've added the component...");
106
- let verified = false;
107
- if (fullPath && fs.existsSync(fullPath)) {
108
- const content = fs.readFileSync(fullPath, "utf-8");
109
- if (content.includes("FeedbackWidget")) {
110
- verified = true;
111
- }
112
- }
113
- if (verified) {
114
- console.log("\n\u2705 Found FeedbackWidget in your file!\n");
119
+ return;
115
120
  }
116
121
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
117
- console.log("\u{1F389} SETUP COMPLETE!\n");
122
+ console.log("\u{1F389} INSTALLED!\n");
118
123
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
119
- console.log(`Run: ${framework.devCommand || "npm run dev"}
120
- `);
121
- console.log("Refresh your browser - the widget will appear in the bottom-right.\n");
122
- console.log("View feedback at: https://feedbackwidget-api.vercel.app/dashboard\n");
124
+ console.log("Next steps:\n");
125
+ console.log(" 1. Run: npm run dev\n");
126
+ console.log(" 2. Refresh your browser\n");
127
+ console.log(" 3. Widget appears in bottom-right corner\n");
128
+ console.log("\nDashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
123
129
  }
124
- function waitForEnter(prompt) {
125
- return new Promise((resolve) => {
126
- process.stdout.write(prompt);
127
- process.stdin.setRawMode?.(false);
128
- process.stdin.resume();
129
- process.stdin.once("data", () => {
130
- resolve();
131
- });
132
- });
130
+ function findNextJsLayout(cwd) {
131
+ const paths = [
132
+ path.join(cwd, "app", "layout.tsx"),
133
+ path.join(cwd, "app", "layout.js"),
134
+ path.join(cwd, "src", "app", "layout.tsx"),
135
+ path.join(cwd, "src", "app", "layout.js")
136
+ ];
137
+ for (const p of paths) {
138
+ if (fs.existsSync(p)) {
139
+ return p;
140
+ }
141
+ }
142
+ return null;
133
143
  }
134
144
  async function whoami() {
135
145
  console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
@@ -194,90 +204,4 @@ function errorPage(error) {
194
204
  </body>
195
205
  </html>`;
196
206
  }
197
- function detectFramework() {
198
- const cwd = process.cwd();
199
- try {
200
- const pkgPath = path.join(cwd, "package.json");
201
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
202
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
203
- if (deps.astro) {
204
- if (fs.existsSync(path.join(cwd, "src", "layouts", "Layout.astro"))) {
205
- return { name: "Astro", file: "src/layouts/Layout.astro", fullPath: "src/layouts/Layout.astro", note: "inside <body>", devCommand: "npm run dev" };
206
- }
207
- if (fs.existsSync(path.join(cwd, "src", "layouts", "BaseLayout.astro"))) {
208
- return { name: "Astro", file: "src/layouts/BaseLayout.astro", fullPath: "src/layouts/BaseLayout.astro", note: "inside <body>", devCommand: "npm run dev" };
209
- }
210
- return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
211
- }
212
- if (deps.next) {
213
- if (fs.existsSync(path.join(cwd, "app", "layout.tsx"))) {
214
- return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
215
- }
216
- if (fs.existsSync(path.join(cwd, "app", "layout.js"))) {
217
- return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
218
- }
219
- if (fs.existsSync(path.join(cwd, "src", "app", "layout.tsx"))) {
220
- return { name: "Next.js (App Router)", file: "src/app/layout.tsx", fullPath: "src/app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
221
- }
222
- if (fs.existsSync(path.join(cwd, "pages", "_app.tsx"))) {
223
- return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
224
- }
225
- if (fs.existsSync(path.join(cwd, "pages", "_app.js"))) {
226
- return { name: "Next.js (Pages Router)", file: "pages/_app.js", fullPath: "pages/_app.js", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
227
- }
228
- }
229
- if (deps["@remix-run/react"]) {
230
- if (fs.existsSync(path.join(cwd, "app", "root.tsx"))) {
231
- return { name: "Remix", file: "app/root.tsx", fullPath: "app/root.tsx", note: "inside <body>", devCommand: "npm run dev" };
232
- }
233
- }
234
- if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
235
- if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
236
- return { name: "Vite + React", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm run dev" };
237
- }
238
- if (fs.existsSync(path.join(cwd, "src", "App.jsx"))) {
239
- return { name: "Vite + React", file: "src/App.jsx", fullPath: "src/App.jsx", note: "at the end of your App component", devCommand: "npm run dev" };
240
- }
241
- }
242
- if (deps["react-scripts"]) {
243
- if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
244
- return { name: "Create React App", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm start" };
245
- }
246
- if (fs.existsSync(path.join(cwd, "src", "App.js"))) {
247
- return { name: "Create React App", file: "src/App.js", fullPath: "src/App.js", note: "at the end of your App component", devCommand: "npm start" };
248
- }
249
- }
250
- if (deps.react) {
251
- return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
252
- }
253
- } catch {
254
- }
255
- return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
256
- }
257
- async function copyToClipboard(text) {
258
- return new Promise((resolve, reject) => {
259
- const platform = process.platform;
260
- let cmd;
261
- let args = [];
262
- if (platform === "darwin") {
263
- cmd = "pbcopy";
264
- } else if (platform === "linux") {
265
- cmd = "xclip";
266
- args = ["-selection", "clipboard"];
267
- } else if (platform === "win32") {
268
- cmd = "clip";
269
- } else {
270
- reject(new Error("Unsupported platform"));
271
- return;
272
- }
273
- const proc = spawn(cmd, args);
274
- proc.stdin.write(text);
275
- proc.stdin.end();
276
- proc.on("close", (code) => {
277
- if (code === 0) resolve();
278
- else reject(new Error(`Clipboard failed with code ${code}`));
279
- });
280
- proc.on("error", reject);
281
- });
282
- }
283
207
  main().catch(console.error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stephenov/feedbackwidget",
3
- "version": "0.7.0",
4
- "description": "Voice-first feedback widget with AI analysis, GitHub, and Slack integration",
3
+ "version": "2.0.0",
4
+ "description": "Voice-first feedback widget for Next.js App Router",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",