@stephenov/feedbackwidget 1.0.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,65 +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 scriptTag = `<script src="https://feedbackwidget-api.vercel.app/widget.js" data-api-key="${apiKey}"></script>`;
95
- console.log("\n\u2705 Authenticated!\n");
96
- if (framework.name) {
97
- console.log(`Detected: ${framework.name}
98
- `);
99
- }
100
- let injected = false;
101
- if (fullPath && import_fs.default.existsSync(fullPath)) {
102
- try {
103
- let content = import_fs.default.readFileSync(fullPath, "utf-8");
104
- if (content.includes("feedbackwidget-api.vercel.app/widget.js")) {
105
- console.log("Widget already installed in this project.\n");
106
- injected = true;
107
- } else {
108
- const headCloseMatch = content.match(/<\/head>/i);
109
- if (headCloseMatch && headCloseMatch.index !== void 0) {
110
- const insertPos = headCloseMatch.index;
111
- const indent = " ";
112
- content = content.slice(0, insertPos) + indent + scriptTag + "\n " + content.slice(insertPos);
113
- import_fs.default.writeFileSync(fullPath, content, "utf-8");
114
- const verify = import_fs.default.readFileSync(fullPath, "utf-8");
115
- if (verify.includes("feedbackwidget-api.vercel.app/widget.js")) {
116
- injected = true;
117
- }
118
- }
119
- }
120
- } catch (err) {
113
+ console.log("\u2713 Authenticated\n");
114
+ console.log("Installing widget...\n");
115
+ let content = import_fs.default.readFileSync(layoutPath, "utf-8");
116
+ const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
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;
121
123
  }
122
124
  }
123
- if (injected) {
124
- 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");
125
- console.log("\u{1F389} WIDGET INSTALLED!\n");
126
- 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");
127
- console.log(`Added to: ${fullPath}
128
- `);
129
- console.log(`Run: ${framework.devCommand || "npm run dev"}
125
+ if (lastImportIndex >= 0) {
126
+ lines.splice(lastImportIndex + 1, 0, importLine);
127
+ content = lines.join("\n");
128
+ }
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}
130
139
  `);
131
- console.log("Refresh your browser - the widget will appear in the bottom-right.\n");
132
- console.log("View feedback at: https://feedbackwidget-api.vercel.app/dashboard\n");
133
- } else {
134
- 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");
135
- console.log("Add this script tag inside <head>:\n");
136
- 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");
137
- console.log(` ${scriptTag}
140
+ console.log(` ${componentLine}
138
141
  `);
139
- try {
140
- await copyToClipboard(scriptTag);
141
- console.log(" \u{1F4CB} Copied to clipboard!\n");
142
- } catch {
142
+ return;
143
+ }
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");
145
+ console.log("\u{1F389} INSTALLED!\n");
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");
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");
152
+ }
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;
143
163
  }
144
- console.log(`File: ${fullPath || framework.file}
145
- `);
146
- console.log(`Then run: ${framework.devCommand || "npm run dev"}
147
- `);
148
- console.log("\nView feedback at: https://feedbackwidget-api.vercel.app/dashboard\n");
149
164
  }
165
+ return null;
150
166
  }
151
167
  async function whoami() {
152
168
  console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
@@ -211,90 +227,4 @@ function errorPage(error) {
211
227
  </body>
212
228
  </html>`;
213
229
  }
214
- function detectFramework() {
215
- const cwd = process.cwd();
216
- try {
217
- const pkgPath = import_path.default.join(cwd, "package.json");
218
- const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
219
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
220
- if (deps.astro) {
221
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "layouts", "Layout.astro"))) {
222
- return { name: "Astro", file: "src/layouts/Layout.astro", fullPath: "src/layouts/Layout.astro", note: "inside <body>", devCommand: "npm run dev" };
223
- }
224
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "layouts", "BaseLayout.astro"))) {
225
- return { name: "Astro", file: "src/layouts/BaseLayout.astro", fullPath: "src/layouts/BaseLayout.astro", note: "inside <body>", devCommand: "npm run dev" };
226
- }
227
- return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
228
- }
229
- if (deps.next) {
230
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.tsx"))) {
231
- return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
232
- }
233
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.js"))) {
234
- return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
235
- }
236
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "app", "layout.tsx"))) {
237
- 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" };
238
- }
239
- if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.tsx"))) {
240
- return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
241
- }
242
- if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.js"))) {
243
- return { name: "Next.js (Pages Router)", file: "pages/_app.js", fullPath: "pages/_app.js", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
244
- }
245
- }
246
- if (deps["@remix-run/react"]) {
247
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "root.tsx"))) {
248
- return { name: "Remix", file: "app/root.tsx", fullPath: "app/root.tsx", note: "inside <body>", devCommand: "npm run dev" };
249
- }
250
- }
251
- if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
252
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
253
- return { name: "Vite + React", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm run dev" };
254
- }
255
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.jsx"))) {
256
- return { name: "Vite + React", file: "src/App.jsx", fullPath: "src/App.jsx", note: "at the end of your App component", devCommand: "npm run dev" };
257
- }
258
- }
259
- if (deps["react-scripts"]) {
260
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
261
- return { name: "Create React App", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm start" };
262
- }
263
- if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.js"))) {
264
- return { name: "Create React App", file: "src/App.js", fullPath: "src/App.js", note: "at the end of your App component", devCommand: "npm start" };
265
- }
266
- }
267
- if (deps.react) {
268
- return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
269
- }
270
- } catch {
271
- }
272
- return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
273
- }
274
- async function copyToClipboard(text) {
275
- return new Promise((resolve, reject) => {
276
- const platform = process.platform;
277
- let cmd;
278
- let args = [];
279
- if (platform === "darwin") {
280
- cmd = "pbcopy";
281
- } else if (platform === "linux") {
282
- cmd = "xclip";
283
- args = ["-selection", "clipboard"];
284
- } else if (platform === "win32") {
285
- cmd = "clip";
286
- } else {
287
- reject(new Error("Unsupported platform"));
288
- return;
289
- }
290
- const proc = (0, import_child_process.spawn)(cmd, args);
291
- proc.stdin.write(text);
292
- proc.stdin.end();
293
- proc.on("close", (code) => {
294
- if (code === 0) resolve();
295
- else reject(new Error(`Clipboard failed with code ${code}`));
296
- });
297
- proc.on("error", reject);
298
- });
299
- }
300
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,65 +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 scriptTag = `<script src="https://feedbackwidget-api.vercel.app/widget.js" data-api-key="${apiKey}"></script>`;
72
- console.log("\n\u2705 Authenticated!\n");
73
- if (framework.name) {
74
- console.log(`Detected: ${framework.name}
75
- `);
76
- }
77
- let injected = false;
78
- if (fullPath && fs.existsSync(fullPath)) {
79
- try {
80
- let content = fs.readFileSync(fullPath, "utf-8");
81
- if (content.includes("feedbackwidget-api.vercel.app/widget.js")) {
82
- console.log("Widget already installed in this project.\n");
83
- injected = true;
84
- } else {
85
- const headCloseMatch = content.match(/<\/head>/i);
86
- if (headCloseMatch && headCloseMatch.index !== void 0) {
87
- const insertPos = headCloseMatch.index;
88
- const indent = " ";
89
- content = content.slice(0, insertPos) + indent + scriptTag + "\n " + content.slice(insertPos);
90
- fs.writeFileSync(fullPath, content, "utf-8");
91
- const verify = fs.readFileSync(fullPath, "utf-8");
92
- if (verify.includes("feedbackwidget-api.vercel.app/widget.js")) {
93
- injected = true;
94
- }
95
- }
96
- }
97
- } catch (err) {
90
+ console.log("\u2713 Authenticated\n");
91
+ console.log("Installing widget...\n");
92
+ let content = fs.readFileSync(layoutPath, "utf-8");
93
+ const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
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;
98
100
  }
99
101
  }
100
- if (injected) {
101
- 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");
102
- console.log("\u{1F389} WIDGET INSTALLED!\n");
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(`Added to: ${fullPath}
105
- `);
106
- console.log(`Run: ${framework.devCommand || "npm run dev"}
102
+ if (lastImportIndex >= 0) {
103
+ lines.splice(lastImportIndex + 1, 0, importLine);
104
+ content = lines.join("\n");
105
+ }
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}
107
116
  `);
108
- console.log("Refresh your browser - the widget will appear in the bottom-right.\n");
109
- console.log("View feedback at: https://feedbackwidget-api.vercel.app/dashboard\n");
110
- } else {
111
- 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");
112
- console.log("Add this script tag inside <head>:\n");
113
- 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");
114
- console.log(` ${scriptTag}
117
+ console.log(` ${componentLine}
115
118
  `);
116
- try {
117
- await copyToClipboard(scriptTag);
118
- console.log(" \u{1F4CB} Copied to clipboard!\n");
119
- } catch {
119
+ return;
120
+ }
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");
122
+ console.log("\u{1F389} INSTALLED!\n");
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");
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");
129
+ }
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;
120
140
  }
121
- console.log(`File: ${fullPath || framework.file}
122
- `);
123
- console.log(`Then run: ${framework.devCommand || "npm run dev"}
124
- `);
125
- console.log("\nView feedback at: https://feedbackwidget-api.vercel.app/dashboard\n");
126
141
  }
142
+ return null;
127
143
  }
128
144
  async function whoami() {
129
145
  console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
@@ -188,90 +204,4 @@ function errorPage(error) {
188
204
  </body>
189
205
  </html>`;
190
206
  }
191
- function detectFramework() {
192
- const cwd = process.cwd();
193
- try {
194
- const pkgPath = path.join(cwd, "package.json");
195
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
196
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
197
- if (deps.astro) {
198
- if (fs.existsSync(path.join(cwd, "src", "layouts", "Layout.astro"))) {
199
- return { name: "Astro", file: "src/layouts/Layout.astro", fullPath: "src/layouts/Layout.astro", note: "inside <body>", devCommand: "npm run dev" };
200
- }
201
- if (fs.existsSync(path.join(cwd, "src", "layouts", "BaseLayout.astro"))) {
202
- return { name: "Astro", file: "src/layouts/BaseLayout.astro", fullPath: "src/layouts/BaseLayout.astro", note: "inside <body>", devCommand: "npm run dev" };
203
- }
204
- return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
205
- }
206
- if (deps.next) {
207
- if (fs.existsSync(path.join(cwd, "app", "layout.tsx"))) {
208
- return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
209
- }
210
- if (fs.existsSync(path.join(cwd, "app", "layout.js"))) {
211
- return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
212
- }
213
- if (fs.existsSync(path.join(cwd, "src", "app", "layout.tsx"))) {
214
- 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" };
215
- }
216
- if (fs.existsSync(path.join(cwd, "pages", "_app.tsx"))) {
217
- return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
218
- }
219
- if (fs.existsSync(path.join(cwd, "pages", "_app.js"))) {
220
- return { name: "Next.js (Pages Router)", file: "pages/_app.js", fullPath: "pages/_app.js", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
221
- }
222
- }
223
- if (deps["@remix-run/react"]) {
224
- if (fs.existsSync(path.join(cwd, "app", "root.tsx"))) {
225
- return { name: "Remix", file: "app/root.tsx", fullPath: "app/root.tsx", note: "inside <body>", devCommand: "npm run dev" };
226
- }
227
- }
228
- if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
229
- if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
230
- return { name: "Vite + React", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm run dev" };
231
- }
232
- if (fs.existsSync(path.join(cwd, "src", "App.jsx"))) {
233
- return { name: "Vite + React", file: "src/App.jsx", fullPath: "src/App.jsx", note: "at the end of your App component", devCommand: "npm run dev" };
234
- }
235
- }
236
- if (deps["react-scripts"]) {
237
- if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
238
- return { name: "Create React App", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm start" };
239
- }
240
- if (fs.existsSync(path.join(cwd, "src", "App.js"))) {
241
- return { name: "Create React App", file: "src/App.js", fullPath: "src/App.js", note: "at the end of your App component", devCommand: "npm start" };
242
- }
243
- }
244
- if (deps.react) {
245
- return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
246
- }
247
- } catch {
248
- }
249
- return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
250
- }
251
- async function copyToClipboard(text) {
252
- return new Promise((resolve, reject) => {
253
- const platform = process.platform;
254
- let cmd;
255
- let args = [];
256
- if (platform === "darwin") {
257
- cmd = "pbcopy";
258
- } else if (platform === "linux") {
259
- cmd = "xclip";
260
- args = ["-selection", "clipboard"];
261
- } else if (platform === "win32") {
262
- cmd = "clip";
263
- } else {
264
- reject(new Error("Unsupported platform"));
265
- return;
266
- }
267
- const proc = spawn(cmd, args);
268
- proc.stdin.write(text);
269
- proc.stdin.end();
270
- proc.on("close", (code) => {
271
- if (code === 0) resolve();
272
- else reject(new Error(`Clipboard failed with code ${code}`));
273
- });
274
- proc.on("error", reject);
275
- });
276
- }
277
207
  main().catch(console.error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stephenov/feedbackwidget",
3
- "version": "1.0.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",