@stephenov/feedbackwidget 0.5.1 → 0.7.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.js CHANGED
@@ -88,50 +88,71 @@ async function initProject() {
88
88
  reject(new Error("Authentication timed out"));
89
89
  }, 5 * 60 * 1e3);
90
90
  });
91
- console.log("\u2705 Authenticated!\n");
92
- const injected = injectWidget(apiKey);
93
- if (injected) {
94
- 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");
95
- console.log(" \u2705 Widget automatically added to your app!\n");
96
- console.log(` Modified: ${injected.file}
97
- `);
98
- 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");
99
- console.log(" Just run your dev server and the widget will appear.\n");
100
- } else {
101
- const snippet = `<FeedbackWidget apiKey="${apiKey}" />`;
102
- let copied = false;
103
- try {
104
- await copyToClipboard(snippet);
105
- copied = true;
106
- } catch {
107
- }
108
- 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");
109
- console.log(" Your API Key:\n");
110
- console.log(` ${apiKey}
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";
96
+ 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}
111
101
  `);
112
- 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");
113
- if (copied) {
114
- console.log(" \u{1F4CB} Copied to clipboard!\n");
115
- }
116
- const framework = detectFramework();
117
- console.log(" Add to your app:\n");
118
- console.log(' import { FeedbackWidget } from "@stephenov/feedbackwidget"\n');
119
- console.log(` ${snippet}
102
+ }
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}
120
107
  `);
121
- if (framework.name) {
122
- console.log(` Detected: ${framework.name}
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");
112
+ }
113
+ console.log(` ${importLine}
123
114
  `);
124
- console.log(` Add to: ${framework.file}
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>"}:
125
120
  `);
126
- if (framework.note) {
127
- console.log(` Note: ${framework.note}
121
+ console.log(` ${componentSnippet}
128
122
  `);
129
- }
130
- } else {
131
- console.log(" Add to your root component (renders on every page)\n");
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;
132
134
  }
133
135
  }
134
- console.log(" Dashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
136
+ if (verified) {
137
+ console.log("\n\u2705 Found FeedbackWidget in your file!\n");
138
+ }
139
+ 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");
141
+ 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");
146
+ }
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
+ });
135
156
  }
136
157
  async function whoami() {
137
158
  console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
@@ -196,170 +217,65 @@ function errorPage(error) {
196
217
  </body>
197
218
  </html>`;
198
219
  }
199
- function injectWidget(apiKey) {
220
+ function detectFramework() {
200
221
  const cwd = process.cwd();
201
- const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
202
- const component = `<FeedbackWidget apiKey="${apiKey}" />`;
203
222
  try {
204
223
  const pkgPath = import_path.default.join(cwd, "package.json");
205
224
  const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
206
225
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
207
- if (deps.next) {
208
- const layoutPaths = [
209
- import_path.default.join(cwd, "app", "layout.tsx"),
210
- import_path.default.join(cwd, "app", "layout.js"),
211
- import_path.default.join(cwd, "src", "app", "layout.tsx"),
212
- import_path.default.join(cwd, "src", "app", "layout.js")
213
- ];
214
- for (const layoutPath of layoutPaths) {
215
- if (import_fs.default.existsSync(layoutPath)) {
216
- let content = import_fs.default.readFileSync(layoutPath, "utf-8");
217
- if (content.includes("FeedbackWidget")) {
218
- return null;
219
- }
220
- const lines = content.split("\n");
221
- let lastImportIndex = -1;
222
- for (let i = 0; i < lines.length; i++) {
223
- if (lines[i].startsWith("import ")) {
224
- lastImportIndex = i;
225
- }
226
- }
227
- if (lastImportIndex >= 0) {
228
- lines.splice(lastImportIndex + 1, 0, importLine);
229
- } else {
230
- lines.unshift(importLine);
231
- }
232
- content = lines.join("\n");
233
- const bodyRegex = /(<body[^>]*>)/;
234
- const bodyMatch = content.match(bodyRegex);
235
- if (bodyMatch && bodyMatch.index !== void 0) {
236
- const insertPos = bodyMatch.index + bodyMatch[0].length;
237
- content = content.slice(0, insertPos) + "\n " + component + content.slice(insertPos);
238
- }
239
- import_fs.default.writeFileSync(layoutPath, content, { encoding: "utf-8", flag: "w" });
240
- const verify = import_fs.default.readFileSync(layoutPath, "utf-8");
241
- if (!verify.includes("FeedbackWidget")) {
242
- console.error(" Warning: File write may not have persisted. Check " + layoutPath);
243
- return null;
244
- }
245
- return { file: layoutPath.replace(cwd + "/", "") };
246
- }
247
- }
248
- const appPaths = [
249
- import_path.default.join(cwd, "pages", "_app.tsx"),
250
- import_path.default.join(cwd, "pages", "_app.js"),
251
- import_path.default.join(cwd, "src", "pages", "_app.tsx"),
252
- import_path.default.join(cwd, "src", "pages", "_app.js")
253
- ];
254
- for (const appPath of appPaths) {
255
- if (import_fs.default.existsSync(appPath)) {
256
- let content = import_fs.default.readFileSync(appPath, "utf-8");
257
- if (content.includes("FeedbackWidget")) {
258
- return null;
259
- }
260
- const importMatch = content.match(/^(import .+\n)+/m);
261
- if (importMatch) {
262
- const lastImportEnd = importMatch.index + importMatch[0].length;
263
- content = content.slice(0, lastImportEnd) + importLine + "\n" + content.slice(lastImportEnd);
264
- }
265
- const componentMatch = content.match(/<Component\s+\{\.\.\.pageProps\}\s*\/>/);
266
- if (componentMatch) {
267
- const insertPos = componentMatch.index + componentMatch[0].length;
268
- content = content.slice(0, insertPos) + "\n " + component + content.slice(insertPos);
269
- }
270
- import_fs.default.writeFileSync(appPath, content);
271
- return { file: appPath.replace(cwd + "/", "") };
272
- }
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" };
273
229
  }
274
- }
275
- if (deps.vite || deps["react-scripts"] || deps.react) {
276
- const appPaths = [
277
- import_path.default.join(cwd, "src", "App.tsx"),
278
- import_path.default.join(cwd, "src", "App.jsx")
279
- ];
280
- for (const appPath of appPaths) {
281
- if (import_fs.default.existsSync(appPath)) {
282
- let content = import_fs.default.readFileSync(appPath, "utf-8");
283
- if (content.includes("FeedbackWidget")) {
284
- return null;
285
- }
286
- const importMatch = content.match(/^(import .+\n)+/m);
287
- if (importMatch) {
288
- const lastImportEnd = importMatch.index + importMatch[0].length;
289
- content = content.slice(0, lastImportEnd) + importLine + "\n" + content.slice(lastImportEnd);
290
- }
291
- const returnMatch = content.match(/return\s*\(\s*\n?\s*(<[A-Za-z>])/);
292
- if (returnMatch) {
293
- const match = content.match(/return\s*\(\s*\n?\s*<[A-Za-z]+[^>]*>/);
294
- if (match) {
295
- const insertPos = match.index + match[0].length;
296
- content = content.slice(0, insertPos) + "\n " + component + content.slice(insertPos);
297
- }
298
- }
299
- import_fs.default.writeFileSync(appPath, content);
300
- return { file: appPath.replace(cwd + "/", "") };
301
- }
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" };
302
232
  }
233
+ return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
303
234
  }
304
- } catch {
305
- }
306
- return null;
307
- }
308
- function detectFramework() {
309
- const cwd = process.cwd();
310
- try {
311
- const pkgPath = import_path.default.join(cwd, "package.json");
312
- const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
313
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
314
- if (deps.next && import_fs.default.existsSync(import_path.default.join(cwd, "app"))) {
235
+ if (deps.next) {
315
236
  if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.tsx"))) {
316
- return { name: "Next.js (App Router)", file: "app/layout.tsx", note: "Inside <body>, before {children}" };
237
+ return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
317
238
  }
318
239
  if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.js"))) {
319
- return { name: "Next.js (App Router)", file: "app/layout.js", note: "Inside <body>, before {children}" };
240
+ return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
320
241
  }
321
242
  if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "app", "layout.tsx"))) {
322
- return { name: "Next.js (App Router)", file: "src/app/layout.tsx", note: "Inside <body>, before {children}" };
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" };
323
244
  }
324
- }
325
- if (deps.next && import_fs.default.existsSync(import_path.default.join(cwd, "pages"))) {
326
245
  if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.tsx"))) {
327
- return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", note: "After <Component {...pageProps} />" };
246
+ return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
328
247
  }
329
248
  if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.js"))) {
330
- return { name: "Next.js (Pages Router)", file: "pages/_app.js", note: "After <Component {...pageProps} />" };
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" };
331
255
  }
332
256
  }
333
257
  if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
334
258
  if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
335
- return { name: "Vite + React", file: "src/App.tsx", note: "At end of App component" };
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" };
336
260
  }
337
261
  if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.jsx"))) {
338
- return { name: "Vite + React", file: "src/App.jsx", note: "At end of App component" };
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" };
339
263
  }
340
264
  }
341
265
  if (deps["react-scripts"]) {
342
266
  if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
343
- return { name: "Create React App", file: "src/App.tsx", note: "At end of App component" };
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" };
344
268
  }
345
269
  if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.js"))) {
346
- return { name: "Create React App", file: "src/App.js", note: "At end of App component" };
347
- }
348
- }
349
- if (deps.astro) {
350
- return { name: "Astro", file: "A React component", note: "Use client:load directive" };
351
- }
352
- if (deps["@remix-run/react"]) {
353
- if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "root.tsx"))) {
354
- return { name: "Remix", file: "app/root.tsx", note: "Inside <body>" };
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" };
355
271
  }
356
272
  }
357
273
  if (deps.react) {
358
- return { name: "React", file: "Your App component", note: "Where it renders on every page" };
274
+ return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
359
275
  }
360
276
  } catch {
361
277
  }
362
- return { name: null, file: "Your root component" };
278
+ return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
363
279
  }
364
280
  async function copyToClipboard(text) {
365
281
  return new Promise((resolve, reject) => {
package/dist/cli.mjs CHANGED
@@ -65,50 +65,71 @@ async function initProject() {
65
65
  reject(new Error("Authentication timed out"));
66
66
  }, 5 * 60 * 1e3);
67
67
  });
68
- console.log("\u2705 Authenticated!\n");
69
- const injected = injectWidget(apiKey);
70
- if (injected) {
71
- 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");
72
- console.log(" \u2705 Widget automatically added to your app!\n");
73
- console.log(` Modified: ${injected.file}
74
- `);
75
- 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");
76
- console.log(" Just run your dev server and the widget will appear.\n");
77
- } else {
78
- const snippet = `<FeedbackWidget apiKey="${apiKey}" />`;
79
- let copied = false;
80
- try {
81
- await copyToClipboard(snippet);
82
- copied = true;
83
- } catch {
84
- }
85
- 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");
86
- console.log(" Your API Key:\n");
87
- console.log(` ${apiKey}
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";
73
+ 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}
88
78
  `);
89
- 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");
90
- if (copied) {
91
- console.log(" \u{1F4CB} Copied to clipboard!\n");
92
- }
93
- const framework = detectFramework();
94
- console.log(" Add to your app:\n");
95
- console.log(' import { FeedbackWidget } from "@stephenov/feedbackwidget"\n');
96
- console.log(` ${snippet}
79
+ }
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}
97
84
  `);
98
- if (framework.name) {
99
- console.log(` Detected: ${framework.name}
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");
89
+ }
90
+ console.log(` ${importLine}
100
91
  `);
101
- console.log(` Add to: ${framework.file}
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>"}:
102
97
  `);
103
- if (framework.note) {
104
- console.log(` Note: ${framework.note}
98
+ console.log(` ${componentSnippet}
105
99
  `);
106
- }
107
- } else {
108
- console.log(" Add to your root component (renders on every page)\n");
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;
109
111
  }
110
112
  }
111
- console.log(" Dashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
113
+ if (verified) {
114
+ console.log("\n\u2705 Found FeedbackWidget in your file!\n");
115
+ }
116
+ 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");
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(`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");
123
+ }
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
+ });
112
133
  }
113
134
  async function whoami() {
114
135
  console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
@@ -173,170 +194,65 @@ function errorPage(error) {
173
194
  </body>
174
195
  </html>`;
175
196
  }
176
- function injectWidget(apiKey) {
197
+ function detectFramework() {
177
198
  const cwd = process.cwd();
178
- const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
179
- const component = `<FeedbackWidget apiKey="${apiKey}" />`;
180
199
  try {
181
200
  const pkgPath = path.join(cwd, "package.json");
182
201
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
183
202
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
184
- if (deps.next) {
185
- const layoutPaths = [
186
- path.join(cwd, "app", "layout.tsx"),
187
- path.join(cwd, "app", "layout.js"),
188
- path.join(cwd, "src", "app", "layout.tsx"),
189
- path.join(cwd, "src", "app", "layout.js")
190
- ];
191
- for (const layoutPath of layoutPaths) {
192
- if (fs.existsSync(layoutPath)) {
193
- let content = fs.readFileSync(layoutPath, "utf-8");
194
- if (content.includes("FeedbackWidget")) {
195
- return null;
196
- }
197
- const lines = content.split("\n");
198
- let lastImportIndex = -1;
199
- for (let i = 0; i < lines.length; i++) {
200
- if (lines[i].startsWith("import ")) {
201
- lastImportIndex = i;
202
- }
203
- }
204
- if (lastImportIndex >= 0) {
205
- lines.splice(lastImportIndex + 1, 0, importLine);
206
- } else {
207
- lines.unshift(importLine);
208
- }
209
- content = lines.join("\n");
210
- const bodyRegex = /(<body[^>]*>)/;
211
- const bodyMatch = content.match(bodyRegex);
212
- if (bodyMatch && bodyMatch.index !== void 0) {
213
- const insertPos = bodyMatch.index + bodyMatch[0].length;
214
- content = content.slice(0, insertPos) + "\n " + component + content.slice(insertPos);
215
- }
216
- fs.writeFileSync(layoutPath, content, { encoding: "utf-8", flag: "w" });
217
- const verify = fs.readFileSync(layoutPath, "utf-8");
218
- if (!verify.includes("FeedbackWidget")) {
219
- console.error(" Warning: File write may not have persisted. Check " + layoutPath);
220
- return null;
221
- }
222
- return { file: layoutPath.replace(cwd + "/", "") };
223
- }
224
- }
225
- const appPaths = [
226
- path.join(cwd, "pages", "_app.tsx"),
227
- path.join(cwd, "pages", "_app.js"),
228
- path.join(cwd, "src", "pages", "_app.tsx"),
229
- path.join(cwd, "src", "pages", "_app.js")
230
- ];
231
- for (const appPath of appPaths) {
232
- if (fs.existsSync(appPath)) {
233
- let content = fs.readFileSync(appPath, "utf-8");
234
- if (content.includes("FeedbackWidget")) {
235
- return null;
236
- }
237
- const importMatch = content.match(/^(import .+\n)+/m);
238
- if (importMatch) {
239
- const lastImportEnd = importMatch.index + importMatch[0].length;
240
- content = content.slice(0, lastImportEnd) + importLine + "\n" + content.slice(lastImportEnd);
241
- }
242
- const componentMatch = content.match(/<Component\s+\{\.\.\.pageProps\}\s*\/>/);
243
- if (componentMatch) {
244
- const insertPos = componentMatch.index + componentMatch[0].length;
245
- content = content.slice(0, insertPos) + "\n " + component + content.slice(insertPos);
246
- }
247
- fs.writeFileSync(appPath, content);
248
- return { file: appPath.replace(cwd + "/", "") };
249
- }
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" };
250
206
  }
251
- }
252
- if (deps.vite || deps["react-scripts"] || deps.react) {
253
- const appPaths = [
254
- path.join(cwd, "src", "App.tsx"),
255
- path.join(cwd, "src", "App.jsx")
256
- ];
257
- for (const appPath of appPaths) {
258
- if (fs.existsSync(appPath)) {
259
- let content = fs.readFileSync(appPath, "utf-8");
260
- if (content.includes("FeedbackWidget")) {
261
- return null;
262
- }
263
- const importMatch = content.match(/^(import .+\n)+/m);
264
- if (importMatch) {
265
- const lastImportEnd = importMatch.index + importMatch[0].length;
266
- content = content.slice(0, lastImportEnd) + importLine + "\n" + content.slice(lastImportEnd);
267
- }
268
- const returnMatch = content.match(/return\s*\(\s*\n?\s*(<[A-Za-z>])/);
269
- if (returnMatch) {
270
- const match = content.match(/return\s*\(\s*\n?\s*<[A-Za-z]+[^>]*>/);
271
- if (match) {
272
- const insertPos = match.index + match[0].length;
273
- content = content.slice(0, insertPos) + "\n " + component + content.slice(insertPos);
274
- }
275
- }
276
- fs.writeFileSync(appPath, content);
277
- return { file: appPath.replace(cwd + "/", "") };
278
- }
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" };
279
209
  }
210
+ return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
280
211
  }
281
- } catch {
282
- }
283
- return null;
284
- }
285
- function detectFramework() {
286
- const cwd = process.cwd();
287
- try {
288
- const pkgPath = path.join(cwd, "package.json");
289
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
290
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
291
- if (deps.next && fs.existsSync(path.join(cwd, "app"))) {
212
+ if (deps.next) {
292
213
  if (fs.existsSync(path.join(cwd, "app", "layout.tsx"))) {
293
- return { name: "Next.js (App Router)", file: "app/layout.tsx", note: "Inside <body>, before {children}" };
214
+ return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
294
215
  }
295
216
  if (fs.existsSync(path.join(cwd, "app", "layout.js"))) {
296
- return { name: "Next.js (App Router)", file: "app/layout.js", note: "Inside <body>, before {children}" };
217
+ return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
297
218
  }
298
219
  if (fs.existsSync(path.join(cwd, "src", "app", "layout.tsx"))) {
299
- return { name: "Next.js (App Router)", file: "src/app/layout.tsx", note: "Inside <body>, before {children}" };
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" };
300
221
  }
301
- }
302
- if (deps.next && fs.existsSync(path.join(cwd, "pages"))) {
303
222
  if (fs.existsSync(path.join(cwd, "pages", "_app.tsx"))) {
304
- return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", note: "After <Component {...pageProps} />" };
223
+ return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
305
224
  }
306
225
  if (fs.existsSync(path.join(cwd, "pages", "_app.js"))) {
307
- return { name: "Next.js (Pages Router)", file: "pages/_app.js", note: "After <Component {...pageProps} />" };
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" };
308
232
  }
309
233
  }
310
234
  if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
311
235
  if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
312
- return { name: "Vite + React", file: "src/App.tsx", note: "At end of App component" };
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" };
313
237
  }
314
238
  if (fs.existsSync(path.join(cwd, "src", "App.jsx"))) {
315
- return { name: "Vite + React", file: "src/App.jsx", note: "At end of App component" };
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" };
316
240
  }
317
241
  }
318
242
  if (deps["react-scripts"]) {
319
243
  if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
320
- return { name: "Create React App", file: "src/App.tsx", note: "At end of App component" };
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" };
321
245
  }
322
246
  if (fs.existsSync(path.join(cwd, "src", "App.js"))) {
323
- return { name: "Create React App", file: "src/App.js", note: "At end of App component" };
324
- }
325
- }
326
- if (deps.astro) {
327
- return { name: "Astro", file: "A React component", note: "Use client:load directive" };
328
- }
329
- if (deps["@remix-run/react"]) {
330
- if (fs.existsSync(path.join(cwd, "app", "root.tsx"))) {
331
- return { name: "Remix", file: "app/root.tsx", note: "Inside <body>" };
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" };
332
248
  }
333
249
  }
334
250
  if (deps.react) {
335
- return { name: "React", file: "Your App component", note: "Where it renders on every page" };
251
+ return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
336
252
  }
337
253
  } catch {
338
254
  }
339
- return { name: null, file: "Your root component" };
255
+ return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
340
256
  }
341
257
  async function copyToClipboard(text) {
342
258
  return new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stephenov/feedbackwidget",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Voice-first feedback widget with AI analysis, GitHub, and Slack integration",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/dist/cli.d.mts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node
package/dist/cli.d.ts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node