@mhosaic/feedback-cli 0.13.0 → 0.14.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/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @mhosaic/feedback-cli
2
+
3
+ CLI that wires the `@mhosaic/feedback` widget into a host app. Detects your framework, writes `.env.local`, updates `.gitignore`, auto-wraps a Vite + React entry, pings the backend to verify CORS/key/endpoint, and installs the guided integration skill into your global Claude Code skills directory.
4
+
5
+ ```bash
6
+ npm i -g @mhosaic/feedback-cli@latest
7
+ # or, without installing:
8
+ npx @mhosaic/feedback-cli@latest <command>
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Commands
14
+
15
+ ### `init` — set up the widget in this project
16
+
17
+ ```bash
18
+ mhosaic-feedback init \
19
+ --api-key <pk_proj_…> \
20
+ --endpoint <https://your-backend.example.com> \
21
+ --yes
22
+ ```
23
+
24
+ - Installs `@mhosaic/feedback` via your detected package manager (pnpm / yarn / npm)
25
+ - Writes `.env.local` with `VITE_FEEDBACK_API_KEY=…` + `VITE_FEEDBACK_ENDPOINT=…` (wrapped in markers so re-running is idempotent)
26
+ - Ensures `.env.local` is in `.gitignore`
27
+ - For Vite + React entries: auto-wraps `src/main.tsx` with `<FeedbackProvider>` (marker comments preserved so `eject` cleans up cleanly)
28
+
29
+ **Flags:**
30
+
31
+ | Flag | Default | Notes |
32
+ |---|---|---|
33
+ | `--api-key <key>` | (prompt) | Required if `--yes`; must start with `pk_proj_` |
34
+ | `--endpoint <url>` | `http://localhost:8000` | Must be `http(s)://`; no whitespace |
35
+ | `--yes`, `-y` | `false` | Skip prompts; require `--api-key` |
36
+ | `--no-install` | (install) | Skip `npm install` (useful in CI) |
37
+ | `--cwd <dir>` | `process.cwd()` | Operate on a different project root |
38
+
39
+ Without `--yes` and `--api-key`, the CLI prompts interactively.
40
+
41
+ ### `doctor` — local health check
42
+
43
+ ```bash
44
+ mhosaic-feedback doctor
45
+ ```
46
+
47
+ Verifies (read-only):
48
+ - A supported framework was detected
49
+ - `.env.local` has `VITE_FEEDBACK_API_KEY=…`
50
+ - `.gitignore` ignores `.env.local`
51
+ - `<FeedbackProvider>` is wired in the entry file
52
+
53
+ Exits 0 if all pass, 1 otherwise. Run after `init` or whenever you suspect the integration drifted.
54
+
55
+ ### `verify` — end-to-end backend handshake
56
+
57
+ ```bash
58
+ mhosaic-feedback verify --origin <dev-origin> [--with-test-report]
59
+ ```
60
+
61
+ Reads `.env.local`, then checks:
62
+ 1. Both env vars present and well-formed
63
+ 2. Endpoint is `http(s)://…`
64
+ 3. Endpoint resolves and responds
65
+ 4. CORS preflight from `--origin` succeeds
66
+ 5. (with `--with-test-report`) Submits a synthetic `[mhosaic-feedback verify]` report and prints the resulting `report.id`
67
+
68
+ Use `--origin` to match your dev server (`http://localhost:5173` for Vite, `http://localhost:3000` for Next/Remix). The test report path is destructive — it adds one row to the project's report stream. Delete it from `/reports` in the admin SPA if you don't want the noise.
69
+
70
+ ### `install-skill` — drop the Claude Code skill in `~/.claude/skills/`
71
+
72
+ ```bash
73
+ mhosaic-feedback install-skill [--force] [--dry-run] [--dest <dir>]
74
+ ```
75
+
76
+ Copies the bundled `integrate-feedback` skill into your global Claude Code skills directory. Run once after installing the CLI; the skill is then invokable as `/integrate-feedback` from any project. Use `--force` to overwrite existing files (e.g. after upgrading the CLI).
77
+
78
+ ### `eject` — strip the widget out of this project
79
+
80
+ ```bash
81
+ mhosaic-feedback eject
82
+ ```
83
+
84
+ Removes the marker-wrapped blocks `init` added: import + provider wrap in the entry file, and the env var block in `.env.local`. Won't touch anything outside those markers. To finish:
85
+
86
+ ```bash
87
+ pnpm remove @mhosaic/feedback # or npm/yarn equivalent
88
+ ```
89
+
90
+ ---
91
+
92
+ ## The guided skill
93
+
94
+ After `install-skill`, run `/integrate-feedback` inside Claude Code. The skill branches on **operator** vs **consumer** mode and walks you through provisioning a project, generating a public key, and wiring the widget into your specific framework — with explicit checkpoints, framework-aware snippets, and a smoke test that proves the end-to-end path works before declaring done.
95
+
96
+ Full details: `skills/integrate-feedback/SKILL.md` in this package, or installed at `~/.claude/skills/integrate-feedback/SKILL.md` after `install-skill`.
97
+
98
+ ---
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ pnpm install
104
+ pnpm --filter @mhosaic/feedback-cli build # tsup → dist/
105
+ pnpm --filter @mhosaic/feedback-cli exec vitest # unit tests
106
+ node packages/cli/dist/bin.js <command> # exercise locally
107
+ ```
108
+
109
+ The skill files at `packages/cli/skills/integrate-feedback/` are bundled into the published npm package (`files` in `package.json`) and copied to the user's `~/.claude/skills/` by `install-skill`. Edits to the skill flow through the same `npm publish` pipeline as the CLI itself.
package/dist/bin.js CHANGED
@@ -15,16 +15,26 @@ async function main() {
15
15
  const { runEject } = await import("./eject-VNJVOCQM.js");
16
16
  return runEject(args);
17
17
  }
18
+ if (cmd === "verify") {
19
+ const { runVerify } = await import("./verify-LCXL3WOX.js");
20
+ return runVerify(args);
21
+ }
22
+ if (cmd === "install-skill") {
23
+ const { runInstallSkill } = await import("./install-skill-CIZ3ZQI7.js");
24
+ return runInstallSkill(args);
25
+ }
18
26
  if (cmd === "--version" || cmd === "-v") {
19
- process.stdout.write("0.3.0\n");
27
+ process.stdout.write("0.14.0\n");
20
28
  return;
21
29
  }
22
30
  process.stderr.write(`Unknown command: ${cmd}
23
31
 
24
32
  Usage:
25
- mhosaic-feedback init
26
- mhosaic-feedback doctor
27
- mhosaic-feedback eject
33
+ mhosaic-feedback init Set up the widget in this project
34
+ mhosaic-feedback doctor Check local setup
35
+ mhosaic-feedback verify Ping the backend with the configured key
36
+ mhosaic-feedback eject Strip widget integration from this project
37
+ mhosaic-feedback install-skill Copy the Claude Code skill to ~/.claude/skills
28
38
  `);
29
39
  process.exitCode = 1;
30
40
  }
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 === '--version' || cmd === '-v') {\n process.stdout.write('0.3.0\\n')\n return\n }\n process.stderr.write(`Unknown command: ${cmd}\\n\\nUsage:\\n mhosaic-feedback init\\n mhosaic-feedback doctor\\n mhosaic-feedback eject\\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,eAAe,QAAQ,MAAM;AACvC,YAAQ,OAAO,MAAM,SAAS;AAC9B;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,oBAAoB,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAA4F;AACxI,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.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":[]}
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/install-skill.ts
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { dirname, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import kleur from "kleur";
9
+ function parseArgs(argv) {
10
+ const out = { force: false, dryRun: false, dest: join(homedir(), ".claude", "skills") };
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const a = argv[i];
13
+ if (a === "--force" || a === "-f") out.force = true;
14
+ else if (a === "--dry-run") out.dryRun = true;
15
+ else if (a === "--dest") out.dest = argv[++i] ?? out.dest;
16
+ }
17
+ return out;
18
+ }
19
+ function findSkillsSource() {
20
+ const here = dirname(fileURLToPath(import.meta.url));
21
+ const candidates = [
22
+ join(here, "..", "skills"),
23
+ join(here, "..", "..", "skills")
24
+ ];
25
+ for (const c of candidates) {
26
+ if (existsSync(c)) return c;
27
+ }
28
+ return null;
29
+ }
30
+ function copyRecursive(src, dest, force, dryRun) {
31
+ let copied = 0;
32
+ let skipped = 0;
33
+ if (!dryRun && !existsSync(dest)) mkdirSync(dest, { recursive: true });
34
+ for (const entry of readdirSync(src)) {
35
+ const s = join(src, entry);
36
+ const d = join(dest, entry);
37
+ const st = statSync(s);
38
+ if (st.isDirectory()) {
39
+ const r = copyRecursive(s, d, force, dryRun);
40
+ copied += r.copied;
41
+ skipped += r.skipped;
42
+ } else {
43
+ if (existsSync(d) && !force) {
44
+ skipped++;
45
+ continue;
46
+ }
47
+ if (!dryRun) {
48
+ if (!existsSync(dirname(d))) mkdirSync(dirname(d), { recursive: true });
49
+ writeFileSync(d, readFileSync(s));
50
+ }
51
+ copied++;
52
+ }
53
+ }
54
+ return { copied, skipped };
55
+ }
56
+ async function runInstallSkill(argv) {
57
+ const args = parseArgs(argv);
58
+ const src = findSkillsSource();
59
+ process.stdout.write(kleur.bold("\n\u{1F4E6} Installing Mhosaic Feedback Claude Code skill\n\n"));
60
+ if (src === null) {
61
+ process.stderr.write(kleur.red("Could not locate bundled skills/ directory.\n"));
62
+ process.stderr.write(kleur.gray("Reinstall the CLI: `npm i -g @mhosaic/feedback-cli@latest`\n"));
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ process.stdout.write(kleur.gray(`Source: ${src}
67
+ `));
68
+ process.stdout.write(kleur.gray(`Destination: ${args.dest}
69
+ `));
70
+ if (args.dryRun) process.stdout.write(kleur.yellow("(dry run \u2014 no files will be written)\n"));
71
+ process.stdout.write("\n");
72
+ const { copied, skipped } = copyRecursive(src, args.dest, args.force, args.dryRun);
73
+ process.stdout.write(kleur.green(`\u2713 ${copied} file(s) ${args.dryRun ? "would be " : ""}copied
74
+ `));
75
+ if (skipped > 0) {
76
+ process.stdout.write(kleur.yellow(`\u26A0 ${skipped} file(s) skipped (already exist; use --force to overwrite)
77
+ `));
78
+ }
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");
84
+ }
85
+ export {
86
+ runInstallSkill
87
+ };
88
+ //# sourceMappingURL=install-skill-CIZ3ZQI7.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/verify.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+ import kleur from "kleur";
7
+ function parseArgs(argv) {
8
+ const out = { cwd: process.cwd(), withTestReport: false };
9
+ for (let i = 0; i < argv.length; i++) {
10
+ const a = argv[i];
11
+ if (a === "--cwd") out.cwd = argv[++i] ?? process.cwd();
12
+ else if (a === "--with-test-report") out.withTestReport = true;
13
+ else if (a === "--origin") {
14
+ const v = argv[++i];
15
+ if (v !== void 0) out.origin = v;
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+ function readEnvLocal(cwd) {
21
+ const path = join(cwd, ".env.local");
22
+ if (!existsSync(path)) return { apiKey: null, endpoint: null };
23
+ const text = readFileSync(path, "utf8");
24
+ let apiKey = null;
25
+ let endpoint = null;
26
+ for (const line of text.split(/\r?\n/)) {
27
+ const m = /^([A-Z0-9_]+)=(.*)$/.exec(line);
28
+ if (!m) continue;
29
+ if (m[1] === "VITE_FEEDBACK_API_KEY") apiKey = m[2].trim();
30
+ else if (m[1] === "VITE_FEEDBACK_ENDPOINT") endpoint = m[2].trim();
31
+ }
32
+ return { apiKey, endpoint };
33
+ }
34
+ async function fetchWithTimeout(url, init, timeoutMs) {
35
+ const controller = new AbortController();
36
+ const t = setTimeout(() => controller.abort(), timeoutMs);
37
+ try {
38
+ return await fetch(url, { ...init, signal: controller.signal });
39
+ } finally {
40
+ clearTimeout(t);
41
+ }
42
+ }
43
+ async function runVerify(argv) {
44
+ const args = parseArgs(argv);
45
+ const checks = [];
46
+ process.stdout.write(kleur.bold("\n\u{1F50D} Mhosaic Feedback verify\n\n"));
47
+ const env = readEnvLocal(args.cwd);
48
+ checks.push({
49
+ name: ".env.local exists with both keys",
50
+ ok: env.apiKey !== null && env.endpoint !== null,
51
+ hint: "run `mhosaic-feedback init`"
52
+ });
53
+ if (env.apiKey === null || env.endpoint === null) {
54
+ report(checks);
55
+ return;
56
+ }
57
+ checks.push({
58
+ name: "API key starts with pk_proj_",
59
+ ok: env.apiKey.startsWith("pk_proj_") && env.apiKey.length >= 16,
60
+ detail: env.apiKey.length < 16 ? "suspiciously short" : void 0,
61
+ hint: "mint a new key in the admin SPA (Project keys page)"
62
+ });
63
+ let endpointUrl = null;
64
+ try {
65
+ endpointUrl = new URL(env.endpoint);
66
+ } catch {
67
+ }
68
+ checks.push({
69
+ name: "Endpoint is a valid http(s) URL",
70
+ ok: endpointUrl !== null && /^https?:$/.test(endpointUrl.protocol),
71
+ detail: endpointUrl ? `${endpointUrl.origin}` : void 0,
72
+ hint: "should look like https://your-backend.example.com"
73
+ });
74
+ if (endpointUrl === null || !/^https?:$/.test(endpointUrl.protocol)) {
75
+ report(checks);
76
+ return;
77
+ }
78
+ let reachable = false;
79
+ let reachableDetail = "";
80
+ try {
81
+ const res = await fetchWithTimeout(endpointUrl.origin + "/admin/login/", { method: "GET" }, 5e3);
82
+ reachable = res.status > 0;
83
+ reachableDetail = `HTTP ${res.status}`;
84
+ } catch (err) {
85
+ reachableDetail = err instanceof Error ? err.message : String(err);
86
+ }
87
+ checks.push({
88
+ name: "Endpoint reachable over network",
89
+ ok: reachable,
90
+ detail: reachableDetail,
91
+ hint: "check DNS, firewall, and that the backend is running"
92
+ });
93
+ const origin = args.origin ?? "http://localhost:5173";
94
+ let corsOk = false;
95
+ let corsDetail = "";
96
+ try {
97
+ const res = await fetchWithTimeout(
98
+ endpointUrl.origin + "/api/feedback/v1/reports/",
99
+ {
100
+ method: "OPTIONS",
101
+ headers: {
102
+ "Origin": origin,
103
+ "Access-Control-Request-Method": "POST",
104
+ "Access-Control-Request-Headers": "authorization,content-type"
105
+ }
106
+ },
107
+ 5e3
108
+ );
109
+ const allowOrigin = res.headers.get("access-control-allow-origin");
110
+ corsOk = res.status >= 200 && res.status < 300 && (allowOrigin === origin || allowOrigin === "*");
111
+ corsDetail = allowOrigin ? `allow-origin: ${allowOrigin}` : `HTTP ${res.status}, no allow-origin header`;
112
+ } catch (err) {
113
+ corsDetail = err instanceof Error ? err.message : String(err);
114
+ }
115
+ checks.push({
116
+ name: `CORS allows POST from ${origin}`,
117
+ ok: corsOk,
118
+ detail: corsDetail,
119
+ hint: "add this origin to the project's allowed_origins (admin SPA: Edit project)"
120
+ });
121
+ if (args.withTestReport) {
122
+ let testReportOk = false;
123
+ let testReportDetail = "";
124
+ try {
125
+ const body = new URLSearchParams({
126
+ description: `[mhosaic-feedback verify] CLI smoke test at ${(/* @__PURE__ */ new Date()).toISOString()}`,
127
+ feedback_type: "feature",
128
+ severity: "low",
129
+ page_url: origin,
130
+ user_agent: `mhosaic-feedback-cli`,
131
+ env: "dev"
132
+ });
133
+ const res = await fetchWithTimeout(
134
+ endpointUrl.origin + "/api/feedback/v1/reports/",
135
+ {
136
+ method: "POST",
137
+ headers: {
138
+ "Authorization": `Bearer ${env.apiKey}`,
139
+ "Content-Type": "application/x-www-form-urlencoded",
140
+ "Origin": origin
141
+ },
142
+ body: body.toString()
143
+ },
144
+ 1e4
145
+ );
146
+ let json = null;
147
+ try {
148
+ json = await res.json();
149
+ } catch {
150
+ }
151
+ const reportId = json && typeof json === "object" && "id" in json ? json.id : void 0;
152
+ testReportOk = res.status === 201 && Boolean(reportId);
153
+ testReportDetail = testReportOk ? `submitted (report id: ${reportId}) \u2014 delete it from /reports if you don't want noise` : `HTTP ${res.status} \u2014 ${typeof json === "object" ? JSON.stringify(json) : "(no body)"}`;
154
+ } catch (err) {
155
+ testReportDetail = err instanceof Error ? err.message : String(err);
156
+ }
157
+ checks.push({
158
+ name: "POST /api/feedback/v1/reports/ accepts the public key",
159
+ ok: testReportOk,
160
+ detail: testReportDetail,
161
+ hint: "401 = key invalid or revoked; 403 = origin not allowed; 4xx = serializer rejected payload"
162
+ });
163
+ }
164
+ report(checks);
165
+ }
166
+ function report(checks) {
167
+ let failed = 0;
168
+ for (const c of checks) {
169
+ const icon = c.ok ? kleur.green("\u2713") : kleur.red("\u2717");
170
+ const detail = c.detail ? kleur.gray(` (${c.detail})`) : "";
171
+ const hint = !c.ok && c.hint ? kleur.gray(` \u2014 ${c.hint}`) : "";
172
+ process.stdout.write(`${icon} ${c.name}${detail}${hint}
173
+ `);
174
+ if (!c.ok) failed++;
175
+ }
176
+ if (failed > 0) {
177
+ process.stdout.write(kleur.red(`
178
+ ${failed} check(s) failed.
179
+ `));
180
+ process.exitCode = 1;
181
+ } else {
182
+ process.stdout.write(kleur.green("\nAll checks passed.\n"));
183
+ }
184
+ }
185
+ export {
186
+ runVerify
187
+ };
188
+ //# sourceMappingURL=verify-LCXL3WOX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/verify.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport kleur from 'kleur'\n\ninterface VerifyArgs {\n cwd: string\n withTestReport: boolean\n origin?: string\n}\n\nfunction parseArgs(argv: string[]): VerifyArgs {\n const out: VerifyArgs = { cwd: process.cwd(), withTestReport: false }\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i]!\n if (a === '--cwd') out.cwd = argv[++i] ?? process.cwd()\n else if (a === '--with-test-report') out.withTestReport = true\n else if (a === '--origin') { const v = argv[++i]; if (v !== undefined) out.origin = v }\n }\n return out\n}\n\ninterface EnvFields {\n apiKey: string | null\n endpoint: string | null\n}\n\nfunction readEnvLocal(cwd: string): EnvFields {\n const path = join(cwd, '.env.local')\n if (!existsSync(path)) return { apiKey: null, endpoint: null }\n const text = readFileSync(path, 'utf8')\n let apiKey: string | null = null\n let endpoint: string | null = null\n for (const line of text.split(/\\r?\\n/)) {\n const m = /^([A-Z0-9_]+)=(.*)$/.exec(line)\n if (!m) continue\n if (m[1] === 'VITE_FEEDBACK_API_KEY') apiKey = m[2]!.trim()\n else if (m[1] === 'VITE_FEEDBACK_ENDPOINT') endpoint = m[2]!.trim()\n }\n return { apiKey, endpoint }\n}\n\ninterface CheckResult {\n name: string\n ok: boolean\n detail?: string | undefined\n hint?: string | undefined\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\n const controller = new AbortController()\n const t = setTimeout(() => controller.abort(), timeoutMs)\n try {\n return await fetch(url, { ...init, signal: controller.signal })\n } finally {\n clearTimeout(t)\n }\n}\n\nexport async function runVerify(argv: string[]): Promise<void> {\n const args = parseArgs(argv)\n const checks: CheckResult[] = []\n\n process.stdout.write(kleur.bold('\\n🔍 Mhosaic Feedback verify\\n\\n'))\n\n // 1. .env.local exists\n const env = readEnvLocal(args.cwd)\n checks.push({\n name: '.env.local exists with both keys',\n ok: env.apiKey !== null && env.endpoint !== null,\n hint: 'run `mhosaic-feedback init`',\n })\n\n if (env.apiKey === null || env.endpoint === null) {\n report(checks)\n return\n }\n\n // 2. key format\n checks.push({\n name: 'API key starts with pk_proj_',\n ok: env.apiKey.startsWith('pk_proj_') && env.apiKey.length >= 16,\n detail: env.apiKey.length < 16 ? 'suspiciously short' : undefined,\n hint: 'mint a new key in the admin SPA (Project keys page)',\n })\n\n // 3. endpoint format\n let endpointUrl: URL | null = null\n try {\n endpointUrl = new URL(env.endpoint)\n } catch {\n /* falls through */\n }\n checks.push({\n name: 'Endpoint is a valid http(s) URL',\n ok: endpointUrl !== null && /^https?:$/.test(endpointUrl.protocol),\n detail: endpointUrl ? `${endpointUrl.origin}` : undefined,\n hint: 'should look like https://your-backend.example.com',\n })\n if (endpointUrl === null || !/^https?:$/.test(endpointUrl.protocol)) {\n report(checks)\n return\n }\n\n // 4. endpoint reachable (HEAD root or GET /admin/login/ which always exists)\n let reachable = false\n let reachableDetail = ''\n try {\n const res = await fetchWithTimeout(endpointUrl.origin + '/admin/login/', { method: 'GET' }, 5000)\n reachable = res.status > 0\n reachableDetail = `HTTP ${res.status}`\n } catch (err) {\n reachableDetail = err instanceof Error ? err.message : String(err)\n }\n checks.push({\n name: 'Endpoint reachable over network',\n ok: reachable,\n detail: reachableDetail,\n hint: 'check DNS, firewall, and that the backend is running',\n })\n\n // 5. CORS preflight from the consumer's origin\n // Default origin: from --origin flag, or guessed dev server (http://localhost:5173 — Vite default)\n const origin = args.origin ?? 'http://localhost:5173'\n let corsOk = false\n let corsDetail = ''\n try {\n const res = await fetchWithTimeout(\n endpointUrl.origin + '/api/feedback/v1/reports/',\n {\n method: 'OPTIONS',\n headers: {\n 'Origin': origin,\n 'Access-Control-Request-Method': 'POST',\n 'Access-Control-Request-Headers': 'authorization,content-type',\n },\n },\n 5000,\n )\n const allowOrigin = res.headers.get('access-control-allow-origin')\n corsOk = res.status >= 200 && res.status < 300 && (allowOrigin === origin || allowOrigin === '*')\n corsDetail = allowOrigin ? `allow-origin: ${allowOrigin}` : `HTTP ${res.status}, no allow-origin header`\n } catch (err) {\n corsDetail = err instanceof Error ? err.message : String(err)\n }\n checks.push({\n name: `CORS allows POST from ${origin}`,\n ok: corsOk,\n detail: corsDetail,\n hint: 'add this origin to the project\\'s allowed_origins (admin SPA: Edit project)',\n })\n\n // 6. Optional destructive check: submit a synthetic test report\n if (args.withTestReport) {\n let testReportOk = false\n let testReportDetail = ''\n try {\n const body = new URLSearchParams({\n description: `[mhosaic-feedback verify] CLI smoke test at ${new Date().toISOString()}`,\n feedback_type: 'feature',\n severity: 'low',\n page_url: origin,\n user_agent: `mhosaic-feedback-cli`,\n env: 'dev',\n })\n const res = await fetchWithTimeout(\n endpointUrl.origin + '/api/feedback/v1/reports/',\n {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${env.apiKey}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'Origin': origin,\n },\n body: body.toString(),\n },\n 10_000,\n )\n let json: unknown = null\n try { json = await res.json() } catch { /* non-JSON body */ }\n const reportId = json && typeof json === 'object' && 'id' in json ? (json as { id: string }).id : undefined\n testReportOk = res.status === 201 && Boolean(reportId)\n testReportDetail = testReportOk\n ? `submitted (report id: ${reportId}) — delete it from /reports if you don't want noise`\n : `HTTP ${res.status} — ${typeof json === 'object' ? JSON.stringify(json) : '(no body)'}`\n } catch (err) {\n testReportDetail = err instanceof Error ? err.message : String(err)\n }\n checks.push({\n name: 'POST /api/feedback/v1/reports/ accepts the public key',\n ok: testReportOk,\n detail: testReportDetail,\n hint: '401 = key invalid or revoked; 403 = origin not allowed; 4xx = serializer rejected payload',\n })\n }\n\n report(checks)\n}\n\nfunction report(checks: CheckResult[]): void {\n let failed = 0\n for (const c of checks) {\n const icon = c.ok ? kleur.green('✓') : kleur.red('✗')\n const detail = c.detail ? kleur.gray(` (${c.detail})`) : ''\n const hint = !c.ok && c.hint ? kleur.gray(` — ${c.hint}`) : ''\n process.stdout.write(`${icon} ${c.name}${detail}${hint}\\n`)\n if (!c.ok) failed++\n }\n if (failed > 0) {\n process.stdout.write(kleur.red(`\\n${failed} check(s) failed.\\n`))\n process.exitCode = 1\n } else {\n process.stdout.write(kleur.green('\\nAll checks passed.\\n'))\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@mhosaic/feedback-cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
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.",
4
5
  "type": "module",
5
6
  "bin": {
6
7
  "mhosaic-feedback": "./dist/bin.js",
7
8
  "@mhosaic/feedback": "./dist/bin.js"
8
9
  },
9
10
  "files": [
10
- "dist"
11
+ "dist",
12
+ "skills"
11
13
  ],
12
14
  "dependencies": {
13
15
  "kleur": "^4.1.5",
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: integrate-feedback
3
+ description: Walk a teammate through integrating @mhosaic/feedback — either provisioning a project as the platform operator, or installing the widget in a host app as the consumer. Use when the user mentions adding the Mhosaic feedback widget, onboarding a new app, getting a public key, or wiring the widget into their codebase.
4
+ ---
5
+
6
+ # /integrate-feedback — guided widget integration
7
+
8
+ You are guiding a teammate through integrating the **@mhosaic/feedback** widget into a project. The workflow splits into two modes:
9
+
10
+ - **Operator mode** — the platform admin (Victor or a delegate). Provisions a new Company / Project / public key on the Mhosaic backend. Has admin access to the software-factory admin SPA.
11
+ - **Consumer mode** — a teammate. Installs the widget in their own app, given a key + endpoint from the operator.
12
+
13
+ **Both modes are guided. Never assume — confirm at every checkpoint.**
14
+
15
+ ---
16
+
17
+ ## Step 0: Identify the mode
18
+
19
+ Ask the user which mode they're in. Use `AskUserQuestion` with:
20
+
21
+ - question: `Which side of the integration are you working on?`
22
+ - header: `Mode`
23
+ - options:
24
+ - label: `Provision (operator)`, description: `Create a new Company/Project/key in the Mhosaic backend. Requires admin SPA access.`
25
+ - label: `Install (consumer)`, description: `Add the widget to a host app. You already have a pk_proj_… key + endpoint from the operator.`
26
+ - label: `Both — start with operator, then consumer`, description: `Provision first, then keep going.`
27
+
28
+ Branch on the answer. Each branch is documented in detail under `references/`.
29
+
30
+ ---
31
+
32
+ ## Operator mode — provisioning
33
+
34
+ **Goal:** end-to-end, produce a Markdown "handoff payload" the teammate can paste into Claude Code (or follow manually) to install the widget.
35
+
36
+ **Read** the full step-by-step procedure at:
37
+
38
+ - `references/operator-provision.md`
39
+
40
+ The procedure walks through:
41
+
42
+ 1. Confirming the company exists (or creating it)
43
+ 2. Collecting the project name, slug, and `allowed_origins` (the teammate's deployment URL[s])
44
+ 3. Driving Chrome MCP to the admin SPA at https://software-factory-3tbbu.ondigitalocean.app
45
+ 4. Calling the DRF API from the *already-authenticated* browser context (no token copy-paste)
46
+ 5. Minting a `pk_proj_…` widget key and capturing its plaintext value
47
+ 6. Assembling and printing the handoff payload
48
+
49
+ **Crucial:** the operator must already be SSO-logged-in to software-factory before the skill drives Chrome. If they're not, the skill detects this and walks them through `/login` first.
50
+
51
+ After the operator branch finishes, ask whether they want to continue into consumer mode in the same conversation (helpful when they're integrating into one of their own apps).
52
+
53
+ ---
54
+
55
+ ## Consumer mode — installing
56
+
57
+ **Goal:** end-to-end, the widget loads on the host app and a smoke-test report lands in the admin.
58
+
59
+ **Read** the framework-aware install procedure at:
60
+
61
+ - `references/consumer-install.md`
62
+
63
+ The procedure walks through:
64
+
65
+ 1. Verifying we're in a JS/TS project (reads `package.json`)
66
+ 2. Detecting the framework (Vite/Next/Remix/Vue/SvelteKit/CDN)
67
+ 3. Accepting the handoff payload — either pasted verbatim or via `--from-clipboard`
68
+ 4. Running `npx @mhosaic/feedback-cli init --api-key=… --endpoint=… --yes` (non-interactive)
69
+ 5. For non-Vite-React frameworks: pasting the framework-specific snippet from `references/consumer-install-<framework>.md` and asking the user to apply it
70
+ 6. Wiring `fb.identify()` against the consumer's auth provider — recipes in `references/identify-snippets.md`
71
+ 7. Optionally wiring `withErrorTracking` + `withReplay` + `withWebVitals`
72
+ 8. Running `mhosaic-feedback verify --origin <consumer dev origin>` to confirm CORS, key, and endpoint
73
+ 9. Starting the consumer's dev server and driving Chrome to verify the FAB renders
74
+ 10. Submitting a `[INTEGRATE-FEEDBACK SMOKE TEST]` synthetic report from the FAB
75
+ 11. Driving Chrome to software-factory's `/reports` and confirming the smoke-test report appears
76
+
77
+ The smoke-test workflow lives at:
78
+
79
+ - `references/verify-install.md`
80
+
81
+ ---
82
+
83
+ ## Principles to follow
84
+
85
+ - **Confirm at every fork.** Never assume a default. Use `AskUserQuestion` for choices, plain text questions for free-form input.
86
+ - **Show, don't tell.** When you can run a command and show output, run it. When you can drive Chrome and screenshot, drive Chrome.
87
+ - **Idempotent and reversible.** Every state-changing step has a stated rollback (e.g. revoke key in admin SPA, run `mhosaic-feedback eject`). Surface that to the user.
88
+ - **Stop on first red.** If the smoke test fails, do not silently retry. Read `references/verify-install.md`'s diagnostic table, narrow the cause, fix it, then re-run from the failing step.
89
+ - **Don't paste secret keys into git.** Walk the user through adding `.env.local` to `.gitignore` (CLI does this automatically; double-check).
90
+ - **Don't claim "done" without proof.** End the workflow with a screenshot of the smoke-test report landing in admin OR with a clearly enumerated red checklist of what's still broken.
91
+
92
+ ---
93
+
94
+ ## Operator → consumer handoff payload format
95
+
96
+ Always produce this Markdown block at the end of operator mode. The consumer pastes it as the very first message to Claude after running `/integrate-feedback` in their own repo:
97
+
98
+ ````markdown
99
+ ## Mhosaic Feedback handoff
100
+
101
+ **Project slug:** `your-project-slug`
102
+ **Endpoint:** `https://software-factory-3tbbu.ondigitalocean.app`
103
+ **Public widget key:** `pk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxx`
104
+ **Allowed origin(s):** `https://your-app.example.com`, `http://localhost:5173`
105
+
106
+ To install:
107
+ ```bash
108
+ npx @mhosaic/feedback-cli@latest init \
109
+ --api-key pk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxx \
110
+ --endpoint https://software-factory-3tbbu.ondigitalocean.app \
111
+ --yes
112
+ ```
113
+
114
+ Then verify:
115
+ ```bash
116
+ npx @mhosaic/feedback-cli@latest verify \
117
+ --origin http://localhost:5173 \
118
+ --with-test-report
119
+ ```
120
+ ````
121
+
122
+ The skill's consumer mode parses this block automatically when pasted as input.
123
+
124
+ ---
125
+
126
+ ## Skill files
127
+
128
+ ```
129
+ skills/integrate-feedback/
130
+ ├── SKILL.md # this file (mode router)
131
+ ├── references/
132
+ │ ├── operator-provision.md # Chrome MCP → admin SPA → DRF
133
+ │ ├── consumer-install.md # framework router
134
+ │ ├── consumer-install-vite.md # Vite + React (CLI auto-wires)
135
+ │ ├── consumer-install-next.md # Next.js App Router + Pages
136
+ │ ├── consumer-install-remix.md # Remix root.tsx
137
+ │ ├── consumer-install-vue.md # Vue 3 + Vite
138
+ │ ├── consumer-install-svelte.md # SvelteKit
139
+ │ ├── consumer-install-plain.md # CDN <script> tag
140
+ │ ├── identify-snippets.md # fb.identify() per auth provider
141
+ │ └── verify-install.md # smoke test + diagnostics
142
+ ```