@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 +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +73 -143
- package/dist/cli.mjs +73 -143
- package/package.json +2 -2
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
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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": "
|
|
4
|
-
"description": "Voice-first feedback widget
|
|
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",
|