@stephenov/feedbackwidget 0.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +70 -146
- package/dist/cli.mjs +70 -146
- 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,71 +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 targetFile = fullPath || framework.file;
|
|
95
|
-
const isAstro = framework.name === "Astro";
|
|
113
|
+
console.log("\u2713 Authenticated\n");
|
|
114
|
+
console.log("Installing widget...\n");
|
|
115
|
+
let content = import_fs.default.readFileSync(layoutPath, "utf-8");
|
|
96
116
|
const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
const componentLine = `<FeedbackWidget apiKey="${apiKey}" />`;
|
|
118
|
+
const lines = content.split("\n");
|
|
119
|
+
let lastImportIndex = -1;
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
if (lines[i].startsWith("import ")) {
|
|
122
|
+
lastImportIndex = i;
|
|
123
|
+
}
|
|
102
124
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log(`Open: ${targetFile}
|
|
107
|
-
`);
|
|
108
|
-
if (isAstro) {
|
|
109
|
-
console.log("Add this inside the --- frontmatter block:\n");
|
|
110
|
-
} else {
|
|
111
|
-
console.log("Add this with your other imports:\n");
|
|
125
|
+
if (lastImportIndex >= 0) {
|
|
126
|
+
lines.splice(lastImportIndex + 1, 0, importLine);
|
|
127
|
+
content = lines.join("\n");
|
|
112
128
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
const bodyMatch = content.match(/<body[^>]*>/);
|
|
130
|
+
if (bodyMatch && bodyMatch.index !== void 0) {
|
|
131
|
+
const insertPos = bodyMatch.index + bodyMatch[0].length;
|
|
132
|
+
content = content.slice(0, insertPos) + "\n " + componentLine + content.slice(insertPos);
|
|
133
|
+
}
|
|
134
|
+
import_fs.default.writeFileSync(layoutPath, content, "utf-8");
|
|
135
|
+
const verify = import_fs.default.readFileSync(layoutPath, "utf-8");
|
|
136
|
+
if (!verify.includes("FeedbackWidget")) {
|
|
137
|
+
console.log("\u274C Failed to install. Please add manually:\n");
|
|
138
|
+
console.log(` ${importLine}
|
|
120
139
|
`);
|
|
121
|
-
|
|
140
|
+
console.log(` ${componentLine}
|
|
122
141
|
`);
|
|
123
|
-
|
|
124
|
-
await copyToClipboard(componentSnippet);
|
|
125
|
-
console.log(" \u{1F4CB} Copied to clipboard!\n");
|
|
126
|
-
} catch {
|
|
127
|
-
}
|
|
128
|
-
await waitForEnter("Press ENTER when you've added the component...");
|
|
129
|
-
let verified = false;
|
|
130
|
-
if (fullPath && import_fs.default.existsSync(fullPath)) {
|
|
131
|
-
const content = import_fs.default.readFileSync(fullPath, "utf-8");
|
|
132
|
-
if (content.includes("FeedbackWidget")) {
|
|
133
|
-
verified = true;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (verified) {
|
|
137
|
-
console.log("\n\u2705 Found FeedbackWidget in your file!\n");
|
|
142
|
+
return;
|
|
138
143
|
}
|
|
139
144
|
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
140
|
-
console.log("\u{1F389}
|
|
145
|
+
console.log("\u{1F389} INSTALLED!\n");
|
|
141
146
|
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
142
|
-
console.log(
|
|
143
|
-
|
|
144
|
-
console.log("Refresh your browser
|
|
145
|
-
console.log("
|
|
147
|
+
console.log("Next steps:\n");
|
|
148
|
+
console.log(" 1. Run: npm run dev\n");
|
|
149
|
+
console.log(" 2. Refresh your browser\n");
|
|
150
|
+
console.log(" 3. Widget appears in bottom-right corner\n");
|
|
151
|
+
console.log("\nDashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
|
|
146
152
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
function findNextJsLayout(cwd) {
|
|
154
|
+
const paths = [
|
|
155
|
+
import_path.default.join(cwd, "app", "layout.tsx"),
|
|
156
|
+
import_path.default.join(cwd, "app", "layout.js"),
|
|
157
|
+
import_path.default.join(cwd, "src", "app", "layout.tsx"),
|
|
158
|
+
import_path.default.join(cwd, "src", "app", "layout.js")
|
|
159
|
+
];
|
|
160
|
+
for (const p of paths) {
|
|
161
|
+
if (import_fs.default.existsSync(p)) {
|
|
162
|
+
return p;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
156
166
|
}
|
|
157
167
|
async function whoami() {
|
|
158
168
|
console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
|
|
@@ -217,90 +227,4 @@ function errorPage(error) {
|
|
|
217
227
|
</body>
|
|
218
228
|
</html>`;
|
|
219
229
|
}
|
|
220
|
-
function detectFramework() {
|
|
221
|
-
const cwd = process.cwd();
|
|
222
|
-
try {
|
|
223
|
-
const pkgPath = import_path.default.join(cwd, "package.json");
|
|
224
|
-
const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf-8"));
|
|
225
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
226
|
-
if (deps.astro) {
|
|
227
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "layouts", "Layout.astro"))) {
|
|
228
|
-
return { name: "Astro", file: "src/layouts/Layout.astro", fullPath: "src/layouts/Layout.astro", note: "inside <body>", devCommand: "npm run dev" };
|
|
229
|
-
}
|
|
230
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "layouts", "BaseLayout.astro"))) {
|
|
231
|
-
return { name: "Astro", file: "src/layouts/BaseLayout.astro", fullPath: "src/layouts/BaseLayout.astro", note: "inside <body>", devCommand: "npm run dev" };
|
|
232
|
-
}
|
|
233
|
-
return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
|
|
234
|
-
}
|
|
235
|
-
if (deps.next) {
|
|
236
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.tsx"))) {
|
|
237
|
-
return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
|
|
238
|
-
}
|
|
239
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "layout.js"))) {
|
|
240
|
-
return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
|
|
241
|
-
}
|
|
242
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "app", "layout.tsx"))) {
|
|
243
|
-
return { name: "Next.js (App Router)", file: "src/app/layout.tsx", fullPath: "src/app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
|
|
244
|
-
}
|
|
245
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.tsx"))) {
|
|
246
|
-
return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
|
|
247
|
-
}
|
|
248
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "pages", "_app.js"))) {
|
|
249
|
-
return { name: "Next.js (Pages Router)", file: "pages/_app.js", fullPath: "pages/_app.js", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (deps["@remix-run/react"]) {
|
|
253
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "app", "root.tsx"))) {
|
|
254
|
-
return { name: "Remix", file: "app/root.tsx", fullPath: "app/root.tsx", note: "inside <body>", devCommand: "npm run dev" };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
|
|
258
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
|
|
259
|
-
return { name: "Vite + React", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm run dev" };
|
|
260
|
-
}
|
|
261
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.jsx"))) {
|
|
262
|
-
return { name: "Vite + React", file: "src/App.jsx", fullPath: "src/App.jsx", note: "at the end of your App component", devCommand: "npm run dev" };
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (deps["react-scripts"]) {
|
|
266
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.tsx"))) {
|
|
267
|
-
return { name: "Create React App", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm start" };
|
|
268
|
-
}
|
|
269
|
-
if (import_fs.default.existsSync(import_path.default.join(cwd, "src", "App.js"))) {
|
|
270
|
-
return { name: "Create React App", file: "src/App.js", fullPath: "src/App.js", note: "at the end of your App component", devCommand: "npm start" };
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (deps.react) {
|
|
274
|
-
return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
|
|
275
|
-
}
|
|
276
|
-
} catch {
|
|
277
|
-
}
|
|
278
|
-
return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
|
|
279
|
-
}
|
|
280
|
-
async function copyToClipboard(text) {
|
|
281
|
-
return new Promise((resolve, reject) => {
|
|
282
|
-
const platform = process.platform;
|
|
283
|
-
let cmd;
|
|
284
|
-
let args = [];
|
|
285
|
-
if (platform === "darwin") {
|
|
286
|
-
cmd = "pbcopy";
|
|
287
|
-
} else if (platform === "linux") {
|
|
288
|
-
cmd = "xclip";
|
|
289
|
-
args = ["-selection", "clipboard"];
|
|
290
|
-
} else if (platform === "win32") {
|
|
291
|
-
cmd = "clip";
|
|
292
|
-
} else {
|
|
293
|
-
reject(new Error("Unsupported platform"));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
const proc = (0, import_child_process.spawn)(cmd, args);
|
|
297
|
-
proc.stdin.write(text);
|
|
298
|
-
proc.stdin.end();
|
|
299
|
-
proc.on("close", (code) => {
|
|
300
|
-
if (code === 0) resolve();
|
|
301
|
-
else reject(new Error(`Clipboard failed with code ${code}`));
|
|
302
|
-
});
|
|
303
|
-
proc.on("error", reject);
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
230
|
main().catch(console.error);
|
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import http from "http";
|
|
5
5
|
import open from "open";
|
|
6
6
|
import { randomBytes } from "crypto";
|
|
7
|
-
import { spawn } from "child_process";
|
|
8
7
|
import fs from "fs";
|
|
9
8
|
import path from "path";
|
|
10
9
|
var API_BASE = "https://feedbackwidget-api.vercel.app";
|
|
@@ -21,7 +20,31 @@ async function main() {
|
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
22
|
async function initProject() {
|
|
24
|
-
console.log("\n\u{1F3A4} feedbackwidget - Voice-first feedback for
|
|
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,71 +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 targetFile = fullPath || framework.file;
|
|
72
|
-
const isAstro = framework.name === "Astro";
|
|
90
|
+
console.log("\u2713 Authenticated\n");
|
|
91
|
+
console.log("Installing widget...\n");
|
|
92
|
+
let content = fs.readFileSync(layoutPath, "utf-8");
|
|
73
93
|
const importLine = 'import { FeedbackWidget } from "@stephenov/feedbackwidget";';
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
const componentLine = `<FeedbackWidget apiKey="${apiKey}" />`;
|
|
95
|
+
const lines = content.split("\n");
|
|
96
|
+
let lastImportIndex = -1;
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
if (lines[i].startsWith("import ")) {
|
|
99
|
+
lastImportIndex = i;
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.log(`Open: ${targetFile}
|
|
84
|
-
`);
|
|
85
|
-
if (isAstro) {
|
|
86
|
-
console.log("Add this inside the --- frontmatter block:\n");
|
|
87
|
-
} else {
|
|
88
|
-
console.log("Add this with your other imports:\n");
|
|
102
|
+
if (lastImportIndex >= 0) {
|
|
103
|
+
lines.splice(lastImportIndex + 1, 0, importLine);
|
|
104
|
+
content = lines.join("\n");
|
|
89
105
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
const bodyMatch = content.match(/<body[^>]*>/);
|
|
107
|
+
if (bodyMatch && bodyMatch.index !== void 0) {
|
|
108
|
+
const insertPos = bodyMatch.index + bodyMatch[0].length;
|
|
109
|
+
content = content.slice(0, insertPos) + "\n " + componentLine + content.slice(insertPos);
|
|
110
|
+
}
|
|
111
|
+
fs.writeFileSync(layoutPath, content, "utf-8");
|
|
112
|
+
const verify = fs.readFileSync(layoutPath, "utf-8");
|
|
113
|
+
if (!verify.includes("FeedbackWidget")) {
|
|
114
|
+
console.log("\u274C Failed to install. Please add manually:\n");
|
|
115
|
+
console.log(` ${importLine}
|
|
97
116
|
`);
|
|
98
|
-
|
|
117
|
+
console.log(` ${componentLine}
|
|
99
118
|
`);
|
|
100
|
-
|
|
101
|
-
await copyToClipboard(componentSnippet);
|
|
102
|
-
console.log(" \u{1F4CB} Copied to clipboard!\n");
|
|
103
|
-
} catch {
|
|
104
|
-
}
|
|
105
|
-
await waitForEnter("Press ENTER when you've added the component...");
|
|
106
|
-
let verified = false;
|
|
107
|
-
if (fullPath && fs.existsSync(fullPath)) {
|
|
108
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
109
|
-
if (content.includes("FeedbackWidget")) {
|
|
110
|
-
verified = true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (verified) {
|
|
114
|
-
console.log("\n\u2705 Found FeedbackWidget in your file!\n");
|
|
119
|
+
return;
|
|
115
120
|
}
|
|
116
121
|
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
117
|
-
console.log("\u{1F389}
|
|
122
|
+
console.log("\u{1F389} INSTALLED!\n");
|
|
118
123
|
console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
119
|
-
console.log(
|
|
120
|
-
|
|
121
|
-
console.log("Refresh your browser
|
|
122
|
-
console.log("
|
|
124
|
+
console.log("Next steps:\n");
|
|
125
|
+
console.log(" 1. Run: npm run dev\n");
|
|
126
|
+
console.log(" 2. Refresh your browser\n");
|
|
127
|
+
console.log(" 3. Widget appears in bottom-right corner\n");
|
|
128
|
+
console.log("\nDashboard: https://feedbackwidget-api.vercel.app/dashboard\n");
|
|
123
129
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
function findNextJsLayout(cwd) {
|
|
131
|
+
const paths = [
|
|
132
|
+
path.join(cwd, "app", "layout.tsx"),
|
|
133
|
+
path.join(cwd, "app", "layout.js"),
|
|
134
|
+
path.join(cwd, "src", "app", "layout.tsx"),
|
|
135
|
+
path.join(cwd, "src", "app", "layout.js")
|
|
136
|
+
];
|
|
137
|
+
for (const p of paths) {
|
|
138
|
+
if (fs.existsSync(p)) {
|
|
139
|
+
return p;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
133
143
|
}
|
|
134
144
|
async function whoami() {
|
|
135
145
|
console.log("\nRun 'npx feedbackwidget init' to get your API key.\n");
|
|
@@ -194,90 +204,4 @@ function errorPage(error) {
|
|
|
194
204
|
</body>
|
|
195
205
|
</html>`;
|
|
196
206
|
}
|
|
197
|
-
function detectFramework() {
|
|
198
|
-
const cwd = process.cwd();
|
|
199
|
-
try {
|
|
200
|
-
const pkgPath = path.join(cwd, "package.json");
|
|
201
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
202
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
203
|
-
if (deps.astro) {
|
|
204
|
-
if (fs.existsSync(path.join(cwd, "src", "layouts", "Layout.astro"))) {
|
|
205
|
-
return { name: "Astro", file: "src/layouts/Layout.astro", fullPath: "src/layouts/Layout.astro", note: "inside <body>", devCommand: "npm run dev" };
|
|
206
|
-
}
|
|
207
|
-
if (fs.existsSync(path.join(cwd, "src", "layouts", "BaseLayout.astro"))) {
|
|
208
|
-
return { name: "Astro", file: "src/layouts/BaseLayout.astro", fullPath: "src/layouts/BaseLayout.astro", note: "inside <body>", devCommand: "npm run dev" };
|
|
209
|
-
}
|
|
210
|
-
return { name: "Astro", file: "your main layout .astro file", note: "inside <body>", devCommand: "npm run dev" };
|
|
211
|
-
}
|
|
212
|
-
if (deps.next) {
|
|
213
|
-
if (fs.existsSync(path.join(cwd, "app", "layout.tsx"))) {
|
|
214
|
-
return { name: "Next.js (App Router)", file: "app/layout.tsx", fullPath: "app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
|
|
215
|
-
}
|
|
216
|
-
if (fs.existsSync(path.join(cwd, "app", "layout.js"))) {
|
|
217
|
-
return { name: "Next.js (App Router)", file: "app/layout.js", fullPath: "app/layout.js", note: "inside <body>, before {children}", devCommand: "npm run dev" };
|
|
218
|
-
}
|
|
219
|
-
if (fs.existsSync(path.join(cwd, "src", "app", "layout.tsx"))) {
|
|
220
|
-
return { name: "Next.js (App Router)", file: "src/app/layout.tsx", fullPath: "src/app/layout.tsx", note: "inside <body>, before {children}", devCommand: "npm run dev" };
|
|
221
|
-
}
|
|
222
|
-
if (fs.existsSync(path.join(cwd, "pages", "_app.tsx"))) {
|
|
223
|
-
return { name: "Next.js (Pages Router)", file: "pages/_app.tsx", fullPath: "pages/_app.tsx", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
|
|
224
|
-
}
|
|
225
|
-
if (fs.existsSync(path.join(cwd, "pages", "_app.js"))) {
|
|
226
|
-
return { name: "Next.js (Pages Router)", file: "pages/_app.js", fullPath: "pages/_app.js", note: "after <Component {...pageProps} />", devCommand: "npm run dev" };
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (deps["@remix-run/react"]) {
|
|
230
|
-
if (fs.existsSync(path.join(cwd, "app", "root.tsx"))) {
|
|
231
|
-
return { name: "Remix", file: "app/root.tsx", fullPath: "app/root.tsx", note: "inside <body>", devCommand: "npm run dev" };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
if (deps.vite && (deps.react || deps["@vitejs/plugin-react"])) {
|
|
235
|
-
if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
|
|
236
|
-
return { name: "Vite + React", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm run dev" };
|
|
237
|
-
}
|
|
238
|
-
if (fs.existsSync(path.join(cwd, "src", "App.jsx"))) {
|
|
239
|
-
return { name: "Vite + React", file: "src/App.jsx", fullPath: "src/App.jsx", note: "at the end of your App component", devCommand: "npm run dev" };
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (deps["react-scripts"]) {
|
|
243
|
-
if (fs.existsSync(path.join(cwd, "src", "App.tsx"))) {
|
|
244
|
-
return { name: "Create React App", file: "src/App.tsx", fullPath: "src/App.tsx", note: "at the end of your App component", devCommand: "npm start" };
|
|
245
|
-
}
|
|
246
|
-
if (fs.existsSync(path.join(cwd, "src", "App.js"))) {
|
|
247
|
-
return { name: "Create React App", file: "src/App.js", fullPath: "src/App.js", note: "at the end of your App component", devCommand: "npm start" };
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (deps.react) {
|
|
251
|
-
return { name: "React", file: "your main App component", note: "where it renders on every page", devCommand: "npm run dev" };
|
|
252
|
-
}
|
|
253
|
-
} catch {
|
|
254
|
-
}
|
|
255
|
-
return { name: null, file: "your root component", note: "inside the main layout", devCommand: "npm run dev" };
|
|
256
|
-
}
|
|
257
|
-
async function copyToClipboard(text) {
|
|
258
|
-
return new Promise((resolve, reject) => {
|
|
259
|
-
const platform = process.platform;
|
|
260
|
-
let cmd;
|
|
261
|
-
let args = [];
|
|
262
|
-
if (platform === "darwin") {
|
|
263
|
-
cmd = "pbcopy";
|
|
264
|
-
} else if (platform === "linux") {
|
|
265
|
-
cmd = "xclip";
|
|
266
|
-
args = ["-selection", "clipboard"];
|
|
267
|
-
} else if (platform === "win32") {
|
|
268
|
-
cmd = "clip";
|
|
269
|
-
} else {
|
|
270
|
-
reject(new Error("Unsupported platform"));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
const proc = spawn(cmd, args);
|
|
274
|
-
proc.stdin.write(text);
|
|
275
|
-
proc.stdin.end();
|
|
276
|
-
proc.on("close", (code) => {
|
|
277
|
-
if (code === 0) resolve();
|
|
278
|
-
else reject(new Error(`Clipboard failed with code ${code}`));
|
|
279
|
-
});
|
|
280
|
-
proc.on("error", reject);
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
207
|
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stephenov/feedbackwidget",
|
|
3
|
-
"version": "0.
|
|
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",
|