@mhosaic/feedback-cli 0.14.0 → 0.14.1

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/bin.js CHANGED
@@ -4,7 +4,7 @@
4
4
  async function main() {
5
5
  const [, , cmd, ...args] = process.argv;
6
6
  if (!cmd || cmd === "init") {
7
- const { runInit } = await import("./init-64QSAHMB.js");
7
+ const { runInit } = await import("./init-P45CEXDP.js");
8
8
  return runInit(args);
9
9
  }
10
10
  if (cmd === "doctor") {
@@ -20,11 +20,11 @@ async function main() {
20
20
  return runVerify(args);
21
21
  }
22
22
  if (cmd === "install-skill") {
23
- const { runInstallSkill } = await import("./install-skill-CIZ3ZQI7.js");
23
+ const { runInstallSkill } = await import("./install-skill-3OPVFMTK.js");
24
24
  return runInstallSkill(args);
25
25
  }
26
26
  if (cmd === "--version" || cmd === "-v") {
27
- process.stdout.write("0.14.0\n");
27
+ process.stdout.write("0.14.1\n");
28
28
  return;
29
29
  }
30
30
  process.stderr.write(`Unknown command: ${cmd}
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["async function main() {\n const [, , cmd, ...args] = process.argv\n if (!cmd || cmd === 'init') {\n const { runInit } = await import('./commands/init')\n return runInit(args)\n }\n if (cmd === 'doctor') {\n const { runDoctor } = await import('./commands/doctor')\n return runDoctor(args)\n }\n if (cmd === 'eject') {\n const { runEject } = await import('./commands/eject')\n return runEject(args)\n }\n if (cmd === 'verify') {\n const { runVerify } = await import('./commands/verify')\n return runVerify(args)\n }\n if (cmd === 'install-skill') {\n const { runInstallSkill } = await import('./commands/install-skill')\n return runInstallSkill(args)\n }\n if (cmd === '--version' || cmd === '-v') {\n process.stdout.write('0.14.0\\n')\n return\n }\n process.stderr.write(`Unknown command: ${cmd}\\n\\nUsage:\\n mhosaic-feedback init Set up the widget in this project\\n mhosaic-feedback doctor Check local setup\\n mhosaic-feedback verify Ping the backend with the configured key\\n mhosaic-feedback eject Strip widget integration from this project\\n mhosaic-feedback install-skill Copy the Claude Code skill to ~/.claude/skills\\n`)\n process.exitCode = 1\n}\n\nmain().catch((err) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exitCode = 1\n})\n"],"mappings":";;;AAAA,eAAe,OAAO;AACpB,QAAM,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,IAAI,QAAQ;AACnC,MAAI,CAAC,OAAO,QAAQ,QAAQ;AAC1B,UAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAiB;AAClD,WAAO,QAAQ,IAAI;AAAA,EACrB;AACA,MAAI,QAAQ,UAAU;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAmB;AACtD,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,MAAI,QAAQ,SAAS;AACnB,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,qBAAkB;AACpD,WAAO,SAAS,IAAI;AAAA,EACtB;AACA,MAAI,QAAQ,UAAU;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAmB;AACtD,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,MAAI,QAAQ,iBAAiB;AAC3B,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,6BAA0B;AACnE,WAAO,gBAAgB,IAAI;AAAA,EAC7B;AACA,MAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,YAAQ,OAAO,MAAM,UAAU;AAC/B;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,oBAAoB,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAoX;AACha,UAAQ,WAAW;AACrB;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AAC5E,UAAQ,WAAW;AACrB,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/bin.ts"],"sourcesContent":["async function main() {\n const [, , cmd, ...args] = process.argv\n if (!cmd || cmd === 'init') {\n const { runInit } = await import('./commands/init')\n return runInit(args)\n }\n if (cmd === 'doctor') {\n const { runDoctor } = await import('./commands/doctor')\n return runDoctor(args)\n }\n if (cmd === 'eject') {\n const { runEject } = await import('./commands/eject')\n return runEject(args)\n }\n if (cmd === 'verify') {\n const { runVerify } = await import('./commands/verify')\n return runVerify(args)\n }\n if (cmd === 'install-skill') {\n const { runInstallSkill } = await import('./commands/install-skill')\n return runInstallSkill(args)\n }\n if (cmd === '--version' || cmd === '-v') {\n process.stdout.write('0.14.1\\n')\n return\n }\n process.stderr.write(`Unknown command: ${cmd}\\n\\nUsage:\\n mhosaic-feedback init Set up the widget in this project\\n mhosaic-feedback doctor Check local setup\\n mhosaic-feedback verify Ping the backend with the configured key\\n mhosaic-feedback eject Strip widget integration from this project\\n mhosaic-feedback install-skill Copy the Claude Code skill to ~/.claude/skills\\n`)\n process.exitCode = 1\n}\n\nmain().catch((err) => {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`)\n process.exitCode = 1\n})\n"],"mappings":";;;AAAA,eAAe,OAAO;AACpB,QAAM,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,IAAI,QAAQ;AACnC,MAAI,CAAC,OAAO,QAAQ,QAAQ;AAC1B,UAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAiB;AAClD,WAAO,QAAQ,IAAI;AAAA,EACrB;AACA,MAAI,QAAQ,UAAU;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAmB;AACtD,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,MAAI,QAAQ,SAAS;AACnB,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,qBAAkB;AACpD,WAAO,SAAS,IAAI;AAAA,EACtB;AACA,MAAI,QAAQ,UAAU;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAmB;AACtD,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,MAAI,QAAQ,iBAAiB;AAC3B,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,6BAA0B;AACnE,WAAO,gBAAgB,IAAI;AAAA,EAC7B;AACA,MAAI,QAAQ,eAAe,QAAQ,MAAM;AACvC,YAAQ,OAAO,MAAM,UAAU;AAC/B;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,oBAAoB,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAoX;AACha,UAAQ,WAAW;AACrB;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,CAAI;AAC5E,UAAQ,WAAW;AACrB,CAAC;","names":[]}
@@ -71,6 +71,15 @@ async function writeEnvLocal(path, entries) {
71
71
  const existing = existsSync(path) ? await readFile(path, "utf8") : "";
72
72
  await writeFile(path, renderEnv(existing, entries));
73
73
  }
74
+ async function writeEnvLocalExample(path) {
75
+ const existing = existsSync(path) ? await readFile(path, "utf8") : "";
76
+ const rendered = renderEnv(existing, {
77
+ apiKey: "pk_proj_REPLACE_ME_WITH_YOUR_PROJECT_KEY",
78
+ endpoint: "https://software-factory-3tbbu.ondigitalocean.app"
79
+ });
80
+ const header = "# Copy this file to .env.local and replace the placeholder.\n# .env.local is gitignored; this template stays in git so the next\n# developer onboarding the repo sees which keys are required.\n";
81
+ await writeFile(path, header + rendered);
82
+ }
74
83
 
75
84
  // src/edits/gitignore.ts
76
85
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -234,6 +243,8 @@ async function runInit(argv) {
234
243
  }
235
244
  await writeEnvLocal(join(cwd, ".env.local"), { apiKey, endpoint });
236
245
  process.stdout.write(kleur.green("\u2713 Wrote .env.local\n"));
246
+ await writeEnvLocalExample(join(cwd, ".env.local.example"));
247
+ process.stdout.write(kleur.green("\u2713 Wrote .env.local.example (commit this \u2014 your next teammate will thank you)\n"));
237
248
  await ensureGitignore(join(cwd, ".gitignore"));
238
249
  process.stdout.write(kleur.green("\u2713 Updated .gitignore\n"));
239
250
  if (framework.entry && framework.kind === "vite-react") {
@@ -258,4 +269,4 @@ async function runInit(argv) {
258
269
  export {
259
270
  runInit
260
271
  };
261
- //# sourceMappingURL=init-64QSAHMB.js.map
272
+ //# sourceMappingURL=init-P45CEXDP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/init.ts","../src/edits/env.ts","../src/edits/gitignore.ts","../src/edits/react.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { spawnSync } from 'node:child_process'\n\nimport kleur from 'kleur'\nimport prompts from 'prompts'\n\nimport { detectFramework } from '../detect'\nimport { writeEnvLocal, writeEnvLocalExample } from '../edits/env'\nimport { ensureGitignore } from '../edits/gitignore'\nimport { wrapReactEntry } from '../edits/react'\n\ninterface InitArgs {\n apiKey?: string\n endpoint?: string\n yes: boolean\n install: boolean\n cwd: string\n}\n\nfunction parseArgs(argv: string[]): InitArgs {\n const out: InitArgs = { yes: false, install: true, cwd: process.cwd() }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--api-key') { const v = argv[++i]; if (v !== undefined) out.apiKey = v }\n else if (a === '--endpoint') { const v = argv[++i]; if (v !== undefined) out.endpoint = v }\n else if (a === '--yes' || a === '-y') out.yes = true\n else if (a === '--no-install') out.install = false\n else if (a === '--cwd') out.cwd = argv[++i] ?? process.cwd()\n }\n return out\n}\n\nfunction detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'\n if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'\n return 'npm'\n}\n\nexport async function runInit(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const { cwd } = args\n\n process.stdout.write(kleur.bold('\\n⚡ Mhosaic Feedback setup\\n\\n'))\n\n const framework = await detectFramework(cwd)\n process.stdout.write(kleur.gray(`Detected: ${framework.kind}${framework.entry ? ' (' + framework.entry + ')' : ''}\\n`))\n\n let apiKey = args.apiKey\n let endpoint = args.endpoint ?? 'http://localhost:8000'\n if (!args.yes && !apiKey) {\n const answers = await prompts([\n { type: 'text', name: 'endpoint', message: 'Backend endpoint', initial: endpoint },\n { type: 'text', name: 'apiKey', message: 'Public API key (pk_proj_…)', validate: (v: string) => v.startsWith('pk_proj_') || 'must start with pk_proj_' },\n ])\n if (!answers.apiKey) {\n process.stdout.write(kleur.red('Aborted.\\n'))\n process.exitCode = 1\n return\n }\n endpoint = answers.endpoint\n apiKey = answers.apiKey\n }\n if (!apiKey) {\n process.stderr.write('api-key is required (flag or prompt)\\n')\n process.exitCode = 1\n return\n }\n\n const pm = detectPackageManager(cwd)\n if (args.install) {\n process.stdout.write(kleur.gray(`Installing @mhosaic/feedback via ${pm}…\\n`))\n const cmd = pm === 'pnpm' ? ['pnpm', 'add', '@mhosaic/feedback'] : pm === 'yarn' ? ['yarn', 'add', '@mhosaic/feedback'] : ['npm', 'install', '@mhosaic/feedback']\n const res = spawnSync(cmd[0]!, cmd.slice(1), { cwd, stdio: 'inherit' })\n if (res.status !== 0) {\n process.stderr.write(kleur.red('install failed\\n'))\n process.exitCode = res.status ?? 1\n return\n }\n process.stdout.write(kleur.green('✓ Installed @mhosaic/feedback\\n'))\n }\n\n await writeEnvLocal(join(cwd, '.env.local'), { apiKey, endpoint })\n process.stdout.write(kleur.green('✓ Wrote .env.local\\n'))\n\n await writeEnvLocalExample(join(cwd, '.env.local.example'))\n process.stdout.write(kleur.green('✓ Wrote .env.local.example (commit this — your next teammate will thank you)\\n'))\n\n await ensureGitignore(join(cwd, '.gitignore'))\n process.stdout.write(kleur.green('✓ Updated .gitignore\\n'))\n\n if (framework.entry && framework.kind === 'vite-react') {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n await wrapReactEntry(entryPath, {\n apiKeyEnv: 'VITE_FEEDBACK_API_KEY',\n endpointEnv: 'VITE_FEEDBACK_ENDPOINT',\n })\n process.stdout.write(kleur.green(`✓ Wrapped ${framework.entry}\\n`))\n } else {\n process.stdout.write(kleur.yellow(`⚠ Expected entry ${framework.entry} not found — wire <FeedbackProvider> manually.\\n`))\n }\n } else if (framework.kind !== 'vite-react') {\n process.stdout.write(kleur.yellow(`⚠ Auto-wiring is only supported for Vite + React today. For ${framework.kind}, add <FeedbackProvider> manually.\\n`))\n }\n\n process.stdout.write(kleur.bold('\\nDone.\\n'))\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst MARK = '# === mhosaic-feedback ==='\n\nexport interface EnvEntries {\n apiKey: string\n endpoint: string\n}\n\n/**\n * Reject values that could escape the `KEY=value` line in a .env file.\n *\n * Audit R5/M3: the previous version interpolated user input verbatim,\n * so a paste containing `\\n` would inject arbitrary additional\n * variables, and `\"` would unbalance any later parser. The user owns\n * their own machine — this is a defense-in-depth guard against\n * accidental misuse (e.g. paste from a multi-line message) more than\n * an attack surface.\n */\nfunction ensureEnvSafe(field: 'apiKey' | 'endpoint', value: string): string {\n if (/[\\r\\n]/.test(value)) {\n throw new Error(`${field} contains a newline; refusing to write .env.local.`)\n }\n if (value.includes('\"')) {\n throw new Error(`${field} contains a double-quote; refusing to write .env.local.`)\n }\n if (value.includes('\\0')) {\n throw new Error(`${field} contains a NUL byte; refusing to write .env.local.`)\n }\n return value\n}\n\nfunction ensureKnownEndpoint(value: string): string {\n // http(s) only — a `javascript:` or `file:` URL in .env would silently\n // become the configured backend after `pnpm dev`.\n if (!/^https?:\\/\\/[^\\s]+$/i.test(value)) {\n throw new Error(\n `endpoint must be an http(s) URL with no whitespace; got ${JSON.stringify(value)}`,\n )\n }\n return value\n}\n\nfunction ensureApiKey(value: string): string {\n // Trim accidental leading/trailing whitespace — pasted keys frequently\n // arrive with a trailing newline that survives split-on-tab.\n const trimmed = value.trim()\n if (!trimmed.startsWith('pk_proj_')) {\n throw new Error(\n 'apiKey must look like a widget public key (starts with `pk_proj_`).',\n )\n }\n return trimmed\n}\n\nexport function renderEnv(existing: string, entries: EnvEntries): string {\n const apiKey = ensureEnvSafe('apiKey', ensureApiKey(entries.apiKey))\n const endpoint = ensureEnvSafe('endpoint', ensureKnownEndpoint(entries.endpoint))\n const lines = existing.split(/\\r?\\n/)\n const filtered: string[] = []\n let inBlock = false\n for (const line of lines) {\n if (line.trim() === MARK) { inBlock = !inBlock; continue }\n if (inBlock) continue\n if (line.startsWith('VITE_FEEDBACK_API_KEY=') || line.startsWith('VITE_FEEDBACK_ENDPOINT=')) continue\n filtered.push(line)\n }\n // Trim trailing empty lines\n while (filtered.length > 0 && filtered[filtered.length - 1] === '') filtered.pop()\n const block = [\n MARK,\n `VITE_FEEDBACK_API_KEY=${apiKey}`,\n `VITE_FEEDBACK_ENDPOINT=${endpoint}`,\n MARK,\n ].join('\\n')\n return (filtered.length > 0 ? filtered.join('\\n') + '\\n\\n' : '') + block + '\\n'\n}\n\nexport async function writeEnvLocal(path: string, entries: EnvEntries): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n await writeFile(path, renderEnv(existing, entries))\n}\n\n/**\n * Write a `.env.local.example` template alongside `.env.local`. This file\n * IS checked into git (it lives next to .env.local, but only .env.local\n * gets gitignored) — so the second developer joining the repo sees which\n * keys they need to set, with placeholder values, without having to ask.\n *\n * Idempotent: re-running `init` overwrites the marker block but leaves\n * any custom keys the team added between markers untouched.\n */\nexport async function writeEnvLocalExample(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n // Reuse renderEnv with placeholder values so the file shape exactly\n // matches `.env.local` (same marker, same key order, same encoding rules).\n const rendered = renderEnv(existing, {\n apiKey: 'pk_proj_REPLACE_ME_WITH_YOUR_PROJECT_KEY',\n endpoint: 'https://software-factory-3tbbu.ondigitalocean.app',\n })\n // Prepend a one-line header so a developer who opens the file knows\n // it's a template (and that .env.local is the real file).\n const header = '# Copy this file to .env.local and replace the placeholder.\\n# .env.local is gitignored; this template stays in git so the next\\n# developer onboarding the repo sees which keys are required.\\n'\n await writeFile(path, header + rendered)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst REQUIRED = ['.env.local']\n\nexport function renderGitignore(existing: string): string {\n const lines = new Set(existing.split(/\\r?\\n/).map((l) => l.trim()))\n const missing = REQUIRED.filter((entry) => !lines.has(entry))\n if (missing.length === 0) return existing\n const trailingNewline = existing.endsWith('\\n') || existing === ''\n return existing + (trailingNewline ? '' : '\\n') + missing.join('\\n') + '\\n'\n}\n\nexport async function ensureGitignore(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n const next = renderGitignore(existing)\n if (next !== existing) await writeFile(path, next)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\n\nconst IMPORT_MARK_START = '// === mhosaic-feedback:import:start ==='\nconst IMPORT_MARK_END = '// === mhosaic-feedback:import:end ==='\nconst WRAP_MARK_START = '{/* === mhosaic-feedback:wrap:start === */}'\nconst WRAP_MARK_END = '{/* === mhosaic-feedback:wrap:end === */}'\n\nexport interface WrapOptions {\n apiKeyEnv: string // 'VITE_FEEDBACK_API_KEY'\n endpointEnv: string // 'VITE_FEEDBACK_ENDPOINT'\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction buildImportBlock(): string {\n return [\n IMPORT_MARK_START,\n \"import { FeedbackProvider } from '@mhosaic/feedback/react'\",\n IMPORT_MARK_END,\n ].join('\\n')\n}\n\nfunction buildWrapOpen(opts: WrapOptions): string {\n return [\n WRAP_MARK_START,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n ].join('\\n ')\n}\n\nfunction buildWrapClose(): string {\n return ['</FeedbackProvider>', WRAP_MARK_END].join('\\n ')\n}\n\nexport function transformReactEntry(source: string, opts: WrapOptions): string {\n let out = source\n\n // If markers are already present, do an in-place update of the FeedbackProvider\n // props (import block + wrap tag) without restructuring anything — this is what\n // makes re-runs idempotent without needing to parse/unwrap the JSX tree.\n const alreadyWrapped =\n out.includes(IMPORT_MARK_START) && out.includes(WRAP_MARK_START)\n\n if (alreadyWrapped) {\n // Update import block (replace old import line inside the markers)\n out = out.replace(\n new RegExp(`(${escapeRegex(IMPORT_MARK_START)}\\\\n)[^\\\\n]*(\\\\n${escapeRegex(IMPORT_MARK_END)})`),\n `$1import { FeedbackProvider } from '@mhosaic/feedback/react'$2`,\n )\n // Update FeedbackProvider opening tag props\n out = out.replace(\n /<FeedbackProvider[^>]*>/,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n )\n return out\n }\n\n // Fresh insert: strip any partial markers that may exist (safety), then insert.\n out = out.replace(\n new RegExp(`${escapeRegex(IMPORT_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(IMPORT_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n out = out.replace(\n new RegExp(`${escapeRegex(WRAP_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(WRAP_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n\n // Insert import block after the last existing import statement.\n const importRegex = /^(import[^\\n]+\\n)+/m\n const importMatch = out.match(importRegex)\n if (importMatch) {\n const idx = importMatch.index! + importMatch[0].length\n out = out.slice(0, idx) + buildImportBlock() + '\\n\\n' + out.slice(idx)\n } else {\n out = buildImportBlock() + '\\n\\n' + out\n }\n\n // Wrap the App component inside the render call.\n // Pattern A: createRoot(...).render(<jsx>) — covers both single-line and multi-line.\n // Pattern B: ReactDOM.render(<jsx>, document.getElementById(...))\n const renderPatterns: Array<RegExp> = [\n /\\.render\\(\\s*([\\s\\S]*?)\\s*\\)\\s*[,;]?\\s*\\n/,\n /ReactDOM\\.render\\(\\s*([\\s\\S]*?),\\s*document\\.getElementById/,\n ]\n for (const re of renderPatterns) {\n const m = out.match(re)\n if (m && m.index !== undefined) {\n // Strip any trailing argument-list punctuation from the captured JSX —\n // `render(<App />,)` is valid JS but wrapping a trailing `,` inside the\n // FeedbackProvider children would render a literal \",\" as a text node.\n const jsx = m[1]!.replace(/[,;]\\s*$/, '')\n const wrapped = `\\n ${buildWrapOpen(opts)}\\n ${jsx.replace(/\\n/g, '\\n ')}\\n ${buildWrapClose()}\\n `\n const startIdx = m.index + m[0].indexOf(m[1]!)\n out = out.slice(0, startIdx) + wrapped + out.slice(startIdx + m[1]!.length)\n break\n }\n }\n\n return out\n}\n\nexport async function wrapReactEntry(path: string, opts: WrapOptions): Promise<void> {\n const source = await readFile(path, 'utf8')\n const next = transformReactEntry(source, opts)\n if (next !== source) await writeFile(path, next)\n}\n"],"mappings":"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","names":["existsSync","readFile","writeFile","existsSync","readFile","writeFile","existsSync"]}
@@ -77,12 +77,13 @@ async function runInstallSkill(argv) {
77
77
  `));
78
78
  }
79
79
  process.stdout.write("\n");
80
- process.stdout.write(kleur.bold("Next:"));
81
- process.stdout.write(" open Claude Code in any project and run ");
82
- process.stdout.write(kleur.cyan("/integrate-feedback"));
83
- process.stdout.write("\n");
80
+ process.stdout.write(kleur.bold("Next:\n"));
81
+ process.stdout.write(` 1. ${kleur.gray("Restart Claude Code if you have it open \u2014 skills are discovered at session start.")}
82
+ `);
83
+ process.stdout.write(` 2. ${kleur.gray("Open Claude Code in any project and run ")}${kleur.cyan("/integrate-feedback")}
84
+ `);
84
85
  }
85
86
  export {
86
87
  runInstallSkill
87
88
  };
88
- //# sourceMappingURL=install-skill-CIZ3ZQI7.js.map
89
+ //# sourceMappingURL=install-skill-3OPVFMTK.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/install-skill.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport kleur from 'kleur'\n\ninterface Args {\n force: boolean\n dryRun: boolean\n dest: string\n}\n\nfunction parseArgs(argv: string[]): Args {\n const out: Args = { force: false, dryRun: false, dest: join(homedir(), '.claude', 'skills') }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--force' || a === '-f') out.force = true\n else if (a === '--dry-run') out.dryRun = true\n else if (a === '--dest') out.dest = argv[++i] ?? out.dest\n }\n return out\n}\n\n/**\n * The CLI ships the skill directory inside the npm package. At build time\n * tsup emits dist/bin.js; at runtime we walk up to find the package root\n * and resolve `skills/` from there. This works for global installs (npx)\n * and for symlinked monorepo dev installs.\n */\nfunction findSkillsSource(): string | null {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin lives at dist/bin.js — package root is one level up\n const candidates = [\n join(here, '..', 'skills'),\n join(here, '..', '..', 'skills'),\n ]\n for (const c of candidates) {\n if (existsSync(c)) return c\n }\n return null\n}\n\nfunction copyRecursive(src: string, dest: string, force: boolean, dryRun: boolean): { copied: number; skipped: number } {\n let copied = 0\n let skipped = 0\n if (!dryRun && !existsSync(dest)) mkdirSync(dest, { recursive: true })\n for (const entry of readdirSync(src)) {\n const s = join(src, entry)\n const d = join(dest, entry)\n const st = statSync(s)\n if (st.isDirectory()) {\n const r = copyRecursive(s, d, force, dryRun)\n copied += r.copied\n skipped += r.skipped\n } else {\n if (existsSync(d) && !force) {\n skipped++\n continue\n }\n if (!dryRun) {\n if (!existsSync(dirname(d))) mkdirSync(dirname(d), { recursive: true })\n writeFileSync(d, readFileSync(s))\n }\n copied++\n }\n }\n return { copied, skipped }\n}\n\nexport async function runInstallSkill(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const src = findSkillsSource()\n\n process.stdout.write(kleur.bold('\\n📦 Installing Mhosaic Feedback Claude Code skill\\n\\n'))\n\n if (src === null) {\n process.stderr.write(kleur.red('Could not locate bundled skills/ directory.\\n'))\n process.stderr.write(kleur.gray('Reinstall the CLI: `npm i -g @mhosaic/feedback-cli@latest`\\n'))\n process.exitCode = 1\n return\n }\n\n process.stdout.write(kleur.gray(`Source: ${src}\\n`))\n process.stdout.write(kleur.gray(`Destination: ${args.dest}\\n`))\n if (args.dryRun) process.stdout.write(kleur.yellow('(dry run — no files will be written)\\n'))\n process.stdout.write('\\n')\n\n const { copied, skipped } = copyRecursive(src, args.dest, args.force, args.dryRun)\n\n process.stdout.write(kleur.green(`✓ ${copied} file(s) ${args.dryRun ? 'would be ' : ''}copied\\n`))\n if (skipped > 0) {\n process.stdout.write(kleur.yellow(`⚠ ${skipped} file(s) skipped (already exist; use --force to overwrite)\\n`))\n }\n process.stdout.write('\\n')\n process.stdout.write(kleur.bold('Next:'))\n process.stdout.write(' open Claude Code in any project and run ')\n process.stdout.write(kleur.cyan('/integrate-feedback'))\n process.stdout.write('\\n')\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/commands/install-skill.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport kleur from 'kleur'\n\ninterface Args {\n force: boolean\n dryRun: boolean\n dest: string\n}\n\nfunction parseArgs(argv: string[]): Args {\n const out: Args = { force: false, dryRun: false, dest: join(homedir(), '.claude', 'skills') }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--force' || a === '-f') out.force = true\n else if (a === '--dry-run') out.dryRun = true\n else if (a === '--dest') out.dest = argv[++i] ?? out.dest\n }\n return out\n}\n\n/**\n * The CLI ships the skill directory inside the npm package. At build time\n * tsup emits dist/bin.js; at runtime we walk up to find the package root\n * and resolve `skills/` from there. This works for global installs (npx)\n * and for symlinked monorepo dev installs.\n */\nfunction findSkillsSource(): string | null {\n const here = dirname(fileURLToPath(import.meta.url))\n // bin lives at dist/bin.js — package root is one level up\n const candidates = [\n join(here, '..', 'skills'),\n join(here, '..', '..', 'skills'),\n ]\n for (const c of candidates) {\n if (existsSync(c)) return c\n }\n return null\n}\n\nfunction copyRecursive(src: string, dest: string, force: boolean, dryRun: boolean): { copied: number; skipped: number } {\n let copied = 0\n let skipped = 0\n if (!dryRun && !existsSync(dest)) mkdirSync(dest, { recursive: true })\n for (const entry of readdirSync(src)) {\n const s = join(src, entry)\n const d = join(dest, entry)\n const st = statSync(s)\n if (st.isDirectory()) {\n const r = copyRecursive(s, d, force, dryRun)\n copied += r.copied\n skipped += r.skipped\n } else {\n if (existsSync(d) && !force) {\n skipped++\n continue\n }\n if (!dryRun) {\n if (!existsSync(dirname(d))) mkdirSync(dirname(d), { recursive: true })\n writeFileSync(d, readFileSync(s))\n }\n copied++\n }\n }\n return { copied, skipped }\n}\n\nexport async function runInstallSkill(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const src = findSkillsSource()\n\n process.stdout.write(kleur.bold('\\n📦 Installing Mhosaic Feedback Claude Code skill\\n\\n'))\n\n if (src === null) {\n process.stderr.write(kleur.red('Could not locate bundled skills/ directory.\\n'))\n process.stderr.write(kleur.gray('Reinstall the CLI: `npm i -g @mhosaic/feedback-cli@latest`\\n'))\n process.exitCode = 1\n return\n }\n\n process.stdout.write(kleur.gray(`Source: ${src}\\n`))\n process.stdout.write(kleur.gray(`Destination: ${args.dest}\\n`))\n if (args.dryRun) process.stdout.write(kleur.yellow('(dry run — no files will be written)\\n'))\n process.stdout.write('\\n')\n\n const { copied, skipped } = copyRecursive(src, args.dest, args.force, args.dryRun)\n\n process.stdout.write(kleur.green(`✓ ${copied} file(s) ${args.dryRun ? 'would be ' : ''}copied\\n`))\n if (skipped > 0) {\n process.stdout.write(kleur.yellow(`⚠ ${skipped} file(s) skipped (already exist; use --force to overwrite)\\n`))\n }\n process.stdout.write('\\n')\n process.stdout.write(kleur.bold('Next:\\n'))\n process.stdout.write(` 1. ${kleur.gray('Restart Claude Code if you have it open — skills are discovered at session start.')}\\n`)\n process.stdout.write(` 2. ${kleur.gray('Open Claude Code in any project and run ')}${kleur.cyan('/integrate-feedback')}\\n`)\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhosaic/feedback-cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "CLI to install @mhosaic/feedback into a host app, verify the integration, and drop a guided Claude Code skill (/integrate-feedback) into ~/.claude/skills.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,25 @@ Branch on the answer. Each branch is documented in detail under `references/`.
29
29
 
30
30
  ---
31
31
 
32
+ ## Step 0.5: Pre-flight (operator mode only)
33
+
34
+ If the user picked operator (or "Both"), confirm Chrome MCP is configured before going further. Operator mode hard-depends on `mcp__claude-in-chrome__*` tools — without them, the API-driving step at the heart of `references/operator-provision.md` can't run.
35
+
36
+ Check: call `mcp__claude-in-chrome__tabs_context_mcp` once. Possible outcomes:
37
+
38
+ - **Tool returns a tab list (even an empty one)** — Chrome MCP is installed. Proceed.
39
+ - **Tool isn't available / errors with "tool not found"** — Chrome MCP isn't configured on this Claude Code install. Tell the user:
40
+
41
+ > "Operator mode needs the Chrome MCP server (`@anthropic/claude-in-chrome` or equivalent). Without it, the skill can't drive the admin SPA programmatically. Two options:
42
+ > 1. Install Chrome MCP (Claude Code → MCP settings → add the chrome connector), then re-run `/integrate-feedback`.
43
+ > 2. Provision manually via `docs/INTEGRATING.md` (operator-side: open the admin SPA, create the project, mint the key) and then re-run `/integrate-feedback` in consumer mode with the resulting handoff payload."
44
+
45
+ Stop. Don't continue operator mode without MCP.
46
+
47
+ Consumer mode (install) does NOT need Chrome MCP for the install itself — only for the live smoke test in `references/verify-install.md`. If MCP isn't available, the skill can still do everything up to step 9 of consumer-install.md, and the smoke test degrades to "open this URL in your browser yourself and confirm the FAB renders."
48
+
49
+ ---
50
+
32
51
  ## Operator mode — provisioning
33
52
 
34
53
  **Goal:** end-to-end, produce a Markdown "handoff payload" the teammate can paste into Claude Code (or follow manually) to install the widget.
@@ -133,6 +152,8 @@ skills/integrate-feedback/
133
152
  │ ├── consumer-install.md # framework router
134
153
  │ ├── consumer-install-vite.md # Vite + React (CLI auto-wires)
135
154
  │ ├── consumer-install-next.md # Next.js App Router + Pages
155
+ │ ├── consumer-install-nuxt.md # Nuxt 3 (client plugin)
156
+ │ ├── consumer-install-astro.md # Astro (<script> in Layout)
136
157
  │ ├── consumer-install-remix.md # Remix root.tsx
137
158
  │ ├── consumer-install-vue.md # Vue 3 + Vite
138
159
  │ ├── consumer-install-svelte.md # SvelteKit
@@ -0,0 +1,83 @@
1
+ # Consumer install — Astro
2
+
3
+ The CLI's auto-wrap is React-only; for Astro you'll wire the widget into a client-only Astro component (or via the renderer of your choice).
4
+
5
+ ---
6
+
7
+ ## Step 1 — Env vars
8
+
9
+ Astro uses Vite under the hood. Env vars meant for the browser need the `PUBLIC_` prefix:
10
+
11
+ ```diff
12
+ - VITE_FEEDBACK_API_KEY=pk_proj_…
13
+ - VITE_FEEDBACK_ENDPOINT=https://…
14
+ + PUBLIC_FEEDBACK_API_KEY=pk_proj_…
15
+ + PUBLIC_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Step 2 — Add a client-only widget mount
21
+
22
+ Astro doesn't ship JS by default; you have to opt-in with `client:*` directives or via `<script>` blocks. The cleanest path is a `<script>` block in your shared layout:
23
+
24
+ ```astro
25
+ ---
26
+ // src/layouts/Layout.astro
27
+ const apiKey = import.meta.env.PUBLIC_FEEDBACK_API_KEY
28
+ const endpoint = import.meta.env.PUBLIC_FEEDBACK_ENDPOINT
29
+ ---
30
+ <html>
31
+ <head><slot name="head" /></head>
32
+ <body>
33
+ <slot />
34
+
35
+ <script>
36
+ import { createFeedback } from '@mhosaic/feedback'
37
+ import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
38
+ import { withReplay } from '@mhosaic/feedback/replay'
39
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
40
+
41
+ const apiKey = import.meta.env.PUBLIC_FEEDBACK_API_KEY
42
+ const endpoint = import.meta.env.PUBLIC_FEEDBACK_ENDPOINT
43
+ if (apiKey && endpoint) {
44
+ withErrorTracking(withReplay(withWebVitals(createFeedback({
45
+ apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev',
46
+ }))))
47
+ }
48
+ </script>
49
+ </body>
50
+ </html>
51
+ ```
52
+
53
+ Astro will bundle the script and run it on the client. The widget mounts to its own Shadow DOM, independent of Astro's island hydration.
54
+
55
+ ---
56
+
57
+ ## Step 3 — Wire identify()
58
+
59
+ How you do this depends on what auth backs your Astro site:
60
+
61
+ - **Astro auth integration (e.g. `@auth/astro`, Lucia)** — call `fb.identify()` from a client-only component that reads the session via the matching SDK.
62
+ - **Server-rendered session passed to client via cookies / `window.ENV` pattern** — same trick as Remix: in `Layout.astro` render a small inline `<script>` that surfaces the user via `window.__USER`, then call `fb.identify(window.__USER)` from the import-script block.
63
+ - **Anonymous-only / mostly-static site** — use the anonymous snippet from `references/identify-snippets.md` so the FAB renders for everyone.
64
+
65
+ If the site is mostly static (marketing, docs), anonymous is the typical pick — visitors aren't logged in.
66
+
67
+ ---
68
+
69
+ ## Gotchas
70
+
71
+ - **No SSR window:** Astro's component code runs on the server. Only code inside `<script>` blocks (or `client:*`-hydrated components) runs in the browser. Always wrap the widget init inside `<script>`.
72
+ - **View Transitions API:** if you use Astro's `<ViewTransitions />`, the widget mount survives navigations (it's outside Astro's routed area, in `<body>`). Good — no re-mount churn.
73
+ - **Build output type:** the widget needs `output: 'server'` or `output: 'hybrid'` only if you want the SSR-side env injection; for static (`output: 'static'`), env vars are inlined at build time and the widget still works.
74
+
75
+ ---
76
+
77
+ ## Verifying
78
+
79
+ ```bash
80
+ npx @mhosaic/feedback-cli@latest verify --origin http://localhost:4321 --with-test-report
81
+ ```
82
+
83
+ Astro dev defaults to port 4321.
@@ -0,0 +1,108 @@
1
+ # Consumer install — Nuxt 3
2
+
3
+ The CLI's auto-wrap is React-only; for Nuxt you paste the snippet into a client-only plugin.
4
+
5
+ ---
6
+
7
+ ## Step 1 — Env vars
8
+
9
+ Nuxt 3 exposes env vars via `runtimeConfig`. Open `.env` (or `.env.local` — Nuxt reads both) and rename:
10
+
11
+ ```diff
12
+ - VITE_FEEDBACK_API_KEY=pk_proj_…
13
+ - VITE_FEEDBACK_ENDPOINT=https://…
14
+ + NUXT_PUBLIC_FEEDBACK_API_KEY=pk_proj_…
15
+ + NUXT_PUBLIC_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
16
+ ```
17
+
18
+ Add the matching `runtimeConfig.public` entries to `nuxt.config.ts`:
19
+
20
+ ```typescript
21
+ export default defineNuxtConfig({
22
+ runtimeConfig: {
23
+ public: {
24
+ feedbackApiKey: '',
25
+ feedbackEndpoint: '',
26
+ },
27
+ },
28
+ })
29
+ ```
30
+
31
+ Nuxt auto-binds `NUXT_PUBLIC_*` env vars to the matching `runtimeConfig.public.*` keys at runtime.
32
+
33
+ ---
34
+
35
+ ## Step 2 — Add a client-only plugin
36
+
37
+ Create `plugins/feedback.client.ts` (the `.client.ts` suffix tells Nuxt to bundle this only for the browser, not SSR):
38
+
39
+ ```typescript
40
+ import { createFeedback } from '@mhosaic/feedback'
41
+ import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
42
+ import { withReplay } from '@mhosaic/feedback/replay'
43
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
44
+
45
+ export default defineNuxtPlugin(() => {
46
+ const config = useRuntimeConfig()
47
+ const fb = withErrorTracking(
48
+ withReplay(
49
+ withWebVitals(createFeedback({
50
+ apiKey: config.public.feedbackApiKey as string,
51
+ endpoint: config.public.feedbackEndpoint as string,
52
+ env: process.dev ? 'dev' : 'prod',
53
+ }))
54
+ )
55
+ )
56
+ return { provide: { feedback: fb } }
57
+ })
58
+ ```
59
+
60
+ Now `useNuxtApp().$feedback` is available across the app.
61
+
62
+ ---
63
+
64
+ ## Step 3 — Wire identify()
65
+
66
+ Nuxt's auth is whatever you bolt on (sidebase/nuxt-auth, custom, Supabase, etc.). Use the matching snippet from `references/identify-snippets.md`, called from a `watchEffect` inside `app.vue` or a top-level `<script setup>` that re-runs on auth state changes:
67
+
68
+ ```vue
69
+ <!-- app.vue -->
70
+ <script setup lang="ts">
71
+ import { useAuthStore } from '~/stores/auth' // adapt to your auth source
72
+
73
+ const auth = useAuthStore()
74
+ const { $feedback } = useNuxtApp()
75
+
76
+ watchEffect(() => {
77
+ if (auth.user) {
78
+ $feedback.identify({ id: auth.user.id, email: auth.user.email, name: auth.user.name })
79
+ } else {
80
+ $feedback.identify({ id: '', email: '', name: '' })
81
+ }
82
+ })
83
+ </script>
84
+
85
+ <template>
86
+ <NuxtLayout>
87
+ <NuxtPage />
88
+ </NuxtLayout>
89
+ </template>
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Gotchas
95
+
96
+ - **SSR + window:** Nuxt SSRs everything by default. The `.client.ts` suffix on the plugin ensures the widget code never runs on the Nitro server (where `window` doesn't exist).
97
+ - **`useRuntimeConfig()` returns empty strings on first render** if the env vars weren't set — Nuxt warns at startup. Verify your `.env` has the right `NUXT_PUBLIC_*` keys.
98
+ - **Static site mode (`nuxt generate`):** the widget hydrates at runtime, same as Next.js. Generated pages render without the FAB until JS hydrates.
99
+
100
+ ---
101
+
102
+ ## Verifying
103
+
104
+ ```bash
105
+ npx @mhosaic/feedback-cli@latest verify --origin http://localhost:3000 --with-test-report
106
+ ```
107
+
108
+ Nuxt dev defaults to port 3000.
@@ -17,11 +17,13 @@ If the user can't find the payload, walk them through asking the operator for it
17
17
 
18
18
  ---
19
19
 
20
- ## Step 2 — Verify we're in a JS/TS project
20
+ ## Step 2 — Classify the host app
21
21
 
22
- `Read` the user's `package.json`. If the file doesn't exist, stop and ask:
22
+ `Read` the user's `package.json`. Three branches:
23
23
 
24
- > "I don't see a `package.json` in `<cwd>`. Are you in your host app's project root? cd there and re-run `/integrate-feedback`."
24
+ 1. **package.json exists with `dependencies`** continue to Step 3 (framework detection).
25
+ 2. **No package.json, but `index.html` / `templates/` / `app/views/` / similar template directory exists** → this is a non-JS host app (Django, Rails, Hugo, plain HTML/static site, etc.). Skip Steps 3-5 and go directly to **`references/consumer-install-plain.md`** (CDN `<script>` flow). Tell the user: "I see this isn't a JS/TS app — the CDN path is the right one. The npm-package + framework-snippet flow doesn't apply."
26
+ 3. **Neither package.json nor any obvious template root** → ask the user with plain text: "I don't see a `package.json` or a template folder. Are you in your host app's project root? What kind of app is this — JS framework, server-rendered (Django/Rails/Hugo), or something else?" Branch based on the answer.
25
27
 
26
28
  ---
27
29
 
@@ -35,11 +37,15 @@ Read `package.json`'s `dependencies` + `devDependencies` and classify:
35
37
  |---|---|---|---|
36
38
  | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
37
39
  | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
40
+ | `nuxt` | `nuxt` | `consumer-install-nuxt.md` | ❌ paste snippet |
41
+ | `astro` | `astro` | `consumer-install-astro.md` | ❌ paste snippet |
38
42
  | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
39
- | `vue` ≥ 3 | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
43
+ | `vue` ≥ 3 (no `nuxt`) | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
40
44
  | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
41
45
  | none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
42
46
 
47
+ Order matters: check for `nuxt` *before* generic `vue`, since Nuxt apps have both. Same for `astro` vs the underlying renderer (Astro apps can also have React/Vue/Svelte).
48
+
43
49
  Tell the user which framework you detected. Confirm with `AskUserQuestion` (especially if Next.js, since App vs Pages Router branches differently).
44
50
 
45
51
  ---
@@ -187,7 +187,9 @@ Or guide them with Claude Code:
187
187
  (then paste the block above as their first message)
188
188
  ````
189
189
 
190
- Print this to the chat. Encourage the operator to share it via a secure channel (1Password, Slack DM, signed email) — **not in public channels or git**.
190
+ Print this to the chat.
191
+
192
+ **On sharing the payload:** the `pk_proj_…` is a *public* key (it ships in every consumer's widget bundle, just like a Stripe publishable key). The real threat is "someone with the key + a whitelisted origin can submit junk reports against your project" — noise pollution, not a security breach. So **DM it rather than posting it in a public channel**, but you don't need 1Password levels of caution. The actual secrets in this flow (your SSO session, the backend JWT) never leave your browser; only the public artifacts are in the handoff.
191
193
 
192
194
  ---
193
195
 
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/init.ts","../src/edits/env.ts","../src/edits/gitignore.ts","../src/edits/react.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { spawnSync } from 'node:child_process'\n\nimport kleur from 'kleur'\nimport prompts from 'prompts'\n\nimport { detectFramework } from '../detect'\nimport { writeEnvLocal } from '../edits/env'\nimport { ensureGitignore } from '../edits/gitignore'\nimport { wrapReactEntry } from '../edits/react'\n\ninterface InitArgs {\n apiKey?: string\n endpoint?: string\n yes: boolean\n install: boolean\n cwd: string\n}\n\nfunction parseArgs(argv: string[]): InitArgs {\n const out: InitArgs = { yes: false, install: true, cwd: process.cwd() }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--api-key') { const v = argv[++i]; if (v !== undefined) out.apiKey = v }\n else if (a === '--endpoint') { const v = argv[++i]; if (v !== undefined) out.endpoint = v }\n else if (a === '--yes' || a === '-y') out.yes = true\n else if (a === '--no-install') out.install = false\n else if (a === '--cwd') out.cwd = argv[++i] ?? process.cwd()\n }\n return out\n}\n\nfunction detectPackageManager(cwd: string): 'pnpm' | 'yarn' | 'npm' {\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'\n if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'\n return 'npm'\n}\n\nexport async function runInit(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const { cwd } = args\n\n process.stdout.write(kleur.bold('\\n⚡ Mhosaic Feedback setup\\n\\n'))\n\n const framework = await detectFramework(cwd)\n process.stdout.write(kleur.gray(`Detected: ${framework.kind}${framework.entry ? ' (' + framework.entry + ')' : ''}\\n`))\n\n let apiKey = args.apiKey\n let endpoint = args.endpoint ?? 'http://localhost:8000'\n if (!args.yes && !apiKey) {\n const answers = await prompts([\n { type: 'text', name: 'endpoint', message: 'Backend endpoint', initial: endpoint },\n { type: 'text', name: 'apiKey', message: 'Public API key (pk_proj_…)', validate: (v: string) => v.startsWith('pk_proj_') || 'must start with pk_proj_' },\n ])\n if (!answers.apiKey) {\n process.stdout.write(kleur.red('Aborted.\\n'))\n process.exitCode = 1\n return\n }\n endpoint = answers.endpoint\n apiKey = answers.apiKey\n }\n if (!apiKey) {\n process.stderr.write('api-key is required (flag or prompt)\\n')\n process.exitCode = 1\n return\n }\n\n const pm = detectPackageManager(cwd)\n if (args.install) {\n process.stdout.write(kleur.gray(`Installing @mhosaic/feedback via ${pm}…\\n`))\n const cmd = pm === 'pnpm' ? ['pnpm', 'add', '@mhosaic/feedback'] : pm === 'yarn' ? ['yarn', 'add', '@mhosaic/feedback'] : ['npm', 'install', '@mhosaic/feedback']\n const res = spawnSync(cmd[0]!, cmd.slice(1), { cwd, stdio: 'inherit' })\n if (res.status !== 0) {\n process.stderr.write(kleur.red('install failed\\n'))\n process.exitCode = res.status ?? 1\n return\n }\n process.stdout.write(kleur.green('✓ Installed @mhosaic/feedback\\n'))\n }\n\n await writeEnvLocal(join(cwd, '.env.local'), { apiKey, endpoint })\n process.stdout.write(kleur.green('✓ Wrote .env.local\\n'))\n\n await ensureGitignore(join(cwd, '.gitignore'))\n process.stdout.write(kleur.green('✓ Updated .gitignore\\n'))\n\n if (framework.entry && framework.kind === 'vite-react') {\n const entryPath = join(cwd, framework.entry)\n if (existsSync(entryPath)) {\n await wrapReactEntry(entryPath, {\n apiKeyEnv: 'VITE_FEEDBACK_API_KEY',\n endpointEnv: 'VITE_FEEDBACK_ENDPOINT',\n })\n process.stdout.write(kleur.green(`✓ Wrapped ${framework.entry}\\n`))\n } else {\n process.stdout.write(kleur.yellow(`⚠ Expected entry ${framework.entry} not found — wire <FeedbackProvider> manually.\\n`))\n }\n } else if (framework.kind !== 'vite-react') {\n process.stdout.write(kleur.yellow(`⚠ Auto-wiring is only supported for Vite + React today. For ${framework.kind}, add <FeedbackProvider> manually.\\n`))\n }\n\n process.stdout.write(kleur.bold('\\nDone.\\n'))\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst MARK = '# === mhosaic-feedback ==='\n\nexport interface EnvEntries {\n apiKey: string\n endpoint: string\n}\n\n/**\n * Reject values that could escape the `KEY=value` line in a .env file.\n *\n * Audit R5/M3: the previous version interpolated user input verbatim,\n * so a paste containing `\\n` would inject arbitrary additional\n * variables, and `\"` would unbalance any later parser. The user owns\n * their own machine — this is a defense-in-depth guard against\n * accidental misuse (e.g. paste from a multi-line message) more than\n * an attack surface.\n */\nfunction ensureEnvSafe(field: 'apiKey' | 'endpoint', value: string): string {\n if (/[\\r\\n]/.test(value)) {\n throw new Error(`${field} contains a newline; refusing to write .env.local.`)\n }\n if (value.includes('\"')) {\n throw new Error(`${field} contains a double-quote; refusing to write .env.local.`)\n }\n if (value.includes('\\0')) {\n throw new Error(`${field} contains a NUL byte; refusing to write .env.local.`)\n }\n return value\n}\n\nfunction ensureKnownEndpoint(value: string): string {\n // http(s) only — a `javascript:` or `file:` URL in .env would silently\n // become the configured backend after `pnpm dev`.\n if (!/^https?:\\/\\/[^\\s]+$/i.test(value)) {\n throw new Error(\n `endpoint must be an http(s) URL with no whitespace; got ${JSON.stringify(value)}`,\n )\n }\n return value\n}\n\nfunction ensureApiKey(value: string): string {\n // Trim accidental leading/trailing whitespace — pasted keys frequently\n // arrive with a trailing newline that survives split-on-tab.\n const trimmed = value.trim()\n if (!trimmed.startsWith('pk_proj_')) {\n throw new Error(\n 'apiKey must look like a widget public key (starts with `pk_proj_`).',\n )\n }\n return trimmed\n}\n\nexport function renderEnv(existing: string, entries: EnvEntries): string {\n const apiKey = ensureEnvSafe('apiKey', ensureApiKey(entries.apiKey))\n const endpoint = ensureEnvSafe('endpoint', ensureKnownEndpoint(entries.endpoint))\n const lines = existing.split(/\\r?\\n/)\n const filtered: string[] = []\n let inBlock = false\n for (const line of lines) {\n if (line.trim() === MARK) { inBlock = !inBlock; continue }\n if (inBlock) continue\n if (line.startsWith('VITE_FEEDBACK_API_KEY=') || line.startsWith('VITE_FEEDBACK_ENDPOINT=')) continue\n filtered.push(line)\n }\n // Trim trailing empty lines\n while (filtered.length > 0 && filtered[filtered.length - 1] === '') filtered.pop()\n const block = [\n MARK,\n `VITE_FEEDBACK_API_KEY=${apiKey}`,\n `VITE_FEEDBACK_ENDPOINT=${endpoint}`,\n MARK,\n ].join('\\n')\n return (filtered.length > 0 ? filtered.join('\\n') + '\\n\\n' : '') + block + '\\n'\n}\n\nexport async function writeEnvLocal(path: string, entries: EnvEntries): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n await writeFile(path, renderEnv(existing, entries))\n}\n","import { readFile, writeFile } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\n\nconst REQUIRED = ['.env.local']\n\nexport function renderGitignore(existing: string): string {\n const lines = new Set(existing.split(/\\r?\\n/).map((l) => l.trim()))\n const missing = REQUIRED.filter((entry) => !lines.has(entry))\n if (missing.length === 0) return existing\n const trailingNewline = existing.endsWith('\\n') || existing === ''\n return existing + (trailingNewline ? '' : '\\n') + missing.join('\\n') + '\\n'\n}\n\nexport async function ensureGitignore(path: string): Promise<void> {\n const existing = existsSync(path) ? await readFile(path, 'utf8') : ''\n const next = renderGitignore(existing)\n if (next !== existing) await writeFile(path, next)\n}\n","import { readFile, writeFile } from 'node:fs/promises'\n\nconst IMPORT_MARK_START = '// === mhosaic-feedback:import:start ==='\nconst IMPORT_MARK_END = '// === mhosaic-feedback:import:end ==='\nconst WRAP_MARK_START = '{/* === mhosaic-feedback:wrap:start === */}'\nconst WRAP_MARK_END = '{/* === mhosaic-feedback:wrap:end === */}'\n\nexport interface WrapOptions {\n apiKeyEnv: string // 'VITE_FEEDBACK_API_KEY'\n endpointEnv: string // 'VITE_FEEDBACK_ENDPOINT'\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction buildImportBlock(): string {\n return [\n IMPORT_MARK_START,\n \"import { FeedbackProvider } from '@mhosaic/feedback/react'\",\n IMPORT_MARK_END,\n ].join('\\n')\n}\n\nfunction buildWrapOpen(opts: WrapOptions): string {\n return [\n WRAP_MARK_START,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n ].join('\\n ')\n}\n\nfunction buildWrapClose(): string {\n return ['</FeedbackProvider>', WRAP_MARK_END].join('\\n ')\n}\n\nexport function transformReactEntry(source: string, opts: WrapOptions): string {\n let out = source\n\n // If markers are already present, do an in-place update of the FeedbackProvider\n // props (import block + wrap tag) without restructuring anything — this is what\n // makes re-runs idempotent without needing to parse/unwrap the JSX tree.\n const alreadyWrapped =\n out.includes(IMPORT_MARK_START) && out.includes(WRAP_MARK_START)\n\n if (alreadyWrapped) {\n // Update import block (replace old import line inside the markers)\n out = out.replace(\n new RegExp(`(${escapeRegex(IMPORT_MARK_START)}\\\\n)[^\\\\n]*(\\\\n${escapeRegex(IMPORT_MARK_END)})`),\n `$1import { FeedbackProvider } from '@mhosaic/feedback/react'$2`,\n )\n // Update FeedbackProvider opening tag props\n out = out.replace(\n /<FeedbackProvider[^>]*>/,\n `<FeedbackProvider apiKey={import.meta.env.${opts.apiKeyEnv} as string} endpoint={import.meta.env.${opts.endpointEnv} as string} env=\"prod\">`,\n )\n return out\n }\n\n // Fresh insert: strip any partial markers that may exist (safety), then insert.\n out = out.replace(\n new RegExp(`${escapeRegex(IMPORT_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(IMPORT_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n out = out.replace(\n new RegExp(`${escapeRegex(WRAP_MARK_START)}[\\\\s\\\\S]*?${escapeRegex(WRAP_MARK_END)}\\\\n?`, 'g'),\n '',\n )\n\n // Insert import block after the last existing import statement.\n const importRegex = /^(import[^\\n]+\\n)+/m\n const importMatch = out.match(importRegex)\n if (importMatch) {\n const idx = importMatch.index! + importMatch[0].length\n out = out.slice(0, idx) + buildImportBlock() + '\\n\\n' + out.slice(idx)\n } else {\n out = buildImportBlock() + '\\n\\n' + out\n }\n\n // Wrap the App component inside the render call.\n // Pattern A: createRoot(...).render(<jsx>) — covers both single-line and multi-line.\n // Pattern B: ReactDOM.render(<jsx>, document.getElementById(...))\n const renderPatterns: Array<RegExp> = [\n /\\.render\\(\\s*([\\s\\S]*?)\\s*\\)\\s*[,;]?\\s*\\n/,\n /ReactDOM\\.render\\(\\s*([\\s\\S]*?),\\s*document\\.getElementById/,\n ]\n for (const re of renderPatterns) {\n const m = out.match(re)\n if (m && m.index !== undefined) {\n // Strip any trailing argument-list punctuation from the captured JSX —\n // `render(<App />,)` is valid JS but wrapping a trailing `,` inside the\n // FeedbackProvider children would render a literal \",\" as a text node.\n const jsx = m[1]!.replace(/[,;]\\s*$/, '')\n const wrapped = `\\n ${buildWrapOpen(opts)}\\n ${jsx.replace(/\\n/g, '\\n ')}\\n ${buildWrapClose()}\\n `\n const startIdx = m.index + m[0].indexOf(m[1]!)\n out = out.slice(0, startIdx) + wrapped + out.slice(startIdx + m[1]!.length)\n break\n }\n }\n\n return out\n}\n\nexport async function wrapReactEntry(path: string, opts: WrapOptions): Promise<void> {\n const source = await readFile(path, 'utf8')\n const next = transformReactEntry(source, opts)\n if (next !== source) await writeFile(path, next)\n}\n"],"mappings":"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","names":["existsSync","readFile","writeFile","existsSync","readFile","writeFile","existsSync"]}