@mhosaic/feedback-cli 0.13.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/README.md +109 -0
- package/dist/bin.js +15 -5
- package/dist/bin.js.map +1 -1
- package/dist/{init-64QSAHMB.js → init-P45CEXDP.js} +12 -1
- package/dist/init-P45CEXDP.js.map +1 -0
- package/dist/install-skill-3OPVFMTK.js +89 -0
- package/dist/install-skill-3OPVFMTK.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 +163 -0
- package/skills/integrate-feedback/references/consumer-install-astro.md +83 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +133 -0
- package/skills/integrate-feedback/references/consumer-install-nuxt.md +108 -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 +185 -0
- package/skills/integrate-feedback/references/identify-snippets.md +218 -0
- package/skills/integrate-feedback/references/operator-provision.md +235 -0
- package/skills/integrate-feedback/references/verify-install.md +151 -0
- package/dist/init-64QSAHMB.js.map +0 -1
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
|
@@ -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-
|
|
7
|
+
const { runInit } = await import("./init-P45CEXDP.js");
|
|
8
8
|
return runInit(args);
|
|
9
9
|
}
|
|
10
10
|
if (cmd === "doctor") {
|
|
@@ -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-3OPVFMTK.js");
|
|
24
|
+
return runInstallSkill(args);
|
|
25
|
+
}
|
|
18
26
|
if (cmd === "--version" || cmd === "-v") {
|
|
19
|
-
process.stdout.write("0.
|
|
27
|
+
process.stdout.write("0.14.1\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.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-
|
|
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"]}
|
|
@@ -0,0 +1,89 @@
|
|
|
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:\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
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
runInstallSkill
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=install-skill-3OPVFMTK.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'))\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":";;;AAAA,SAAS,YAAY,WAAW,cAAc,aAAa,UAAU,qBAAqB;AAC1F,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAE9B,OAAO,WAAW;AAQlB,SAAS,UAAU,MAAsB;AACvC,QAAM,MAAY,EAAE,OAAO,OAAO,QAAQ,OAAO,MAAM,KAAK,QAAQ,GAAG,WAAW,QAAQ,EAAE;AAC5F,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,MAAM,aAAa,MAAM,KAAM,KAAI,QAAQ;AAAA,aACtC,MAAM,YAAa,KAAI,SAAS;AAAA,aAChC,MAAM,SAAU,KAAI,OAAO,KAAK,EAAE,CAAC,KAAK,IAAI;AAAA,EACvD;AACA,SAAO;AACT;AAQA,SAAS,mBAAkC;AACzC,QAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEnD,QAAM,aAAa;AAAA,IACjB,KAAK,MAAM,MAAM,QAAQ;AAAA,IACzB,KAAK,MAAM,MAAM,MAAM,QAAQ;AAAA,EACjC;AACA,aAAW,KAAK,YAAY;AAC1B,QAAI,WAAW,CAAC,EAAG,QAAO;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,SAAS,cAAc,KAAa,MAAc,OAAgB,QAAsD;AACtH,MAAI,SAAS;AACb,MAAI,UAAU;AACd,MAAI,CAAC,UAAU,CAAC,WAAW,IAAI,EAAG,WAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AACrE,aAAW,SAAS,YAAY,GAAG,GAAG;AACpC,UAAM,IAAI,KAAK,KAAK,KAAK;AACzB,UAAM,IAAI,KAAK,MAAM,KAAK;AAC1B,UAAM,KAAK,SAAS,CAAC;AACrB,QAAI,GAAG,YAAY,GAAG;AACpB,YAAM,IAAI,cAAc,GAAG,GAAG,OAAO,MAAM;AAC3C,gBAAU,EAAE;AACZ,iBAAW,EAAE;AAAA,IACf,OAAO;AACL,UAAI,WAAW,CAAC,KAAK,CAAC,OAAO;AAC3B;AACA;AAAA,MACF;AACA,UAAI,CAAC,QAAQ;AACX,YAAI,CAAC,WAAW,QAAQ,CAAC,CAAC,EAAG,WAAU,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACtE,sBAAc,GAAG,aAAa,CAAC,CAAC;AAAA,MAClC;AACA;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,QAAQ,QAAQ;AAC3B;AAEA,eAAsB,gBAAgB,MAA+B;AACnE,QAAM,OAAO,UAAU,IAAI;AAC3B,QAAM,MAAM,iBAAiB;AAE7B,UAAQ,OAAO,MAAM,MAAM,KAAK,+DAAwD,CAAC;AAEzF,MAAI,QAAQ,MAAM;AAChB,YAAQ,OAAO,MAAM,MAAM,IAAI,+CAA+C,CAAC;AAC/E,YAAQ,OAAO,MAAM,MAAM,KAAK,8DAA8D,CAAC;AAC/F,YAAQ,WAAW;AACnB;AAAA,EACF;AAEA,UAAQ,OAAO,MAAM,MAAM,KAAK,gBAAgB,GAAG;AAAA,CAAI,CAAC;AACxD,UAAQ,OAAO,MAAM,MAAM,KAAK,gBAAgB,KAAK,IAAI;AAAA,CAAI,CAAC;AAC9D,MAAI,KAAK,OAAQ,SAAQ,OAAO,MAAM,MAAM,OAAO,6CAAwC,CAAC;AAC5F,UAAQ,OAAO,MAAM,IAAI;AAEzB,QAAM,EAAE,QAAQ,QAAQ,IAAI,cAAc,KAAK,KAAK,MAAM,KAAK,OAAO,KAAK,MAAM;AAEjF,UAAQ,OAAO,MAAM,MAAM,MAAM,UAAK,MAAM,YAAY,KAAK,SAAS,cAAc,EAAE;AAAA,CAAU,CAAC;AACjG,MAAI,UAAU,GAAG;AACf,YAAQ,OAAO,MAAM,MAAM,OAAO,UAAK,OAAO;AAAA,CAA8D,CAAC;AAAA,EAC/G;AACA,UAAQ,OAAO,MAAM,IAAI;AACzB,UAAQ,OAAO,MAAM,MAAM,KAAK,SAAS,CAAC;AAC1C,UAAQ,OAAO,MAAM,QAAQ,MAAM,KAAK,wFAAmF,CAAC;AAAA,CAAI;AAChI,UAAQ,OAAO,MAAM,QAAQ,MAAM,KAAK,0CAA0C,CAAC,GAAG,MAAM,KAAK,qBAAqB,CAAC;AAAA,CAAI;AAC7H;","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.1",
|
|
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",
|