@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 +109 -0
- package/dist/bin.js +14 -4
- package/dist/bin.js.map +1 -1
- package/dist/install-skill-CIZ3ZQI7.js +88 -0
- package/dist/install-skill-CIZ3ZQI7.js.map +1 -0
- package/dist/verify-LCXL3WOX.js +188 -0
- package/dist/verify-LCXL3WOX.js.map +1 -0
- package/package.json +4 -2
- package/skills/integrate-feedback/SKILL.md +142 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +133 -0
- package/skills/integrate-feedback/references/consumer-install-plain.md +92 -0
- package/skills/integrate-feedback/references/consumer-install-remix.md +93 -0
- package/skills/integrate-feedback/references/consumer-install-svelte.md +87 -0
- package/skills/integrate-feedback/references/consumer-install-vite.md +100 -0
- package/skills/integrate-feedback/references/consumer-install-vue.md +92 -0
- package/skills/integrate-feedback/references/consumer-install.md +179 -0
- package/skills/integrate-feedback/references/identify-snippets.md +218 -0
- package/skills/integrate-feedback/references/operator-provision.md +233 -0
- package/skills/integrate-feedback/references/verify-install.md +151 -0
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
```
|