infernoflow 0.37.0 → 0.37.3

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -517
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,239 +1,10 @@
1
- /**
2
- * infernoflow export
3
- *
4
- * Export the capability contract to external formats so it can travel
5
- * outside the repo — into API docs, service catalogs, spreadsheets, wikis.
6
- *
7
- * Formats:
8
- * openapi OpenAPI 3.1 JSON (stubs for each capability)
9
- * backstage Backstage catalog-info.yaml
10
- * csv Spreadsheet-ready CSV
11
- * markdown Confluence/Notion-ready Markdown table
12
- * json Clean JSON (normalised, no internal infernoflow fields)
13
- *
14
- * Usage:
15
- * infernoflow export --format openapi
16
- * infernoflow export --format backstage --out catalog-info.yaml
17
- * infernoflow export --format csv --out capabilities.csv
18
- * infernoflow export --format markdown
19
- * infernoflow export --format json --out contract-export.json
20
- */
21
-
22
- import * as fs from "node:fs";
23
- import * as path from "node:path";
24
- import { done, warn, info, bold, cyan, gray } from "../ui/output.mjs";
25
-
26
- // ── Contract reader ───────────────────────────────────────────────────────────
27
-
28
- function readContract(infernoDir) {
29
- for (const f of ["contract.json", "capabilities.json"]) {
30
- const p = path.join(infernoDir, f);
31
- if (!fs.existsSync(p)) continue;
32
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
33
- }
34
- return null;
35
- }
36
-
37
- function normaliseCaps(contract) {
38
- const raw = contract?.capabilities || [];
39
- return raw.map(c => {
40
- if (typeof c === "string") return { id: c, description: "", tags: [], status: "active" };
41
- return {
42
- id: c.id || c.name || "unknown",
43
- description: c.description || "",
44
- tags: c.tags || [],
45
- status: c.status || "active",
46
- since: c.since || "",
47
- owner: c.owner || "",
48
- };
49
- });
50
- }
51
-
52
- function readMeta(infernoDir) {
53
- const pkgPath = path.join(path.dirname(infernoDir), "package.json");
54
- try {
55
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
56
- return { name: pkg.name || "my-service", version: pkg.version || "0.0.0", description: pkg.description || "" };
57
- } catch {
58
- return { name: path.basename(path.dirname(infernoDir)), version: "0.0.0", description: "" };
59
- }
60
- }
61
-
62
- // ── Formatters ────────────────────────────────────────────────────────────────
63
-
64
- function toOpenApi(caps, meta) {
65
- const paths = {};
66
- for (const cap of caps) {
67
- const route = `/${cap.id.replace(/_/g, "-")}`;
68
- paths[route] = {
69
- get: {
70
- operationId: cap.id,
71
- summary: cap.description || cap.id,
72
- tags: cap.tags.length ? cap.tags : [meta.name],
73
- parameters: [],
74
- responses: {
75
- "200": { description: "Success", content: { "application/json": { schema: { type: "object" } } } },
76
- "401": { description: "Unauthorized" },
77
- "500": { description: "Internal Server Error" },
78
- },
79
- },
80
- };
81
- }
82
-
83
- return JSON.stringify({
84
- openapi: "3.1.0",
85
- info: {
86
- title: meta.name,
87
- version: meta.version,
88
- description: meta.description || `Capability contract for ${meta.name}`,
89
- },
90
- paths,
91
- components: { schemas: {}, securitySchemes: {} },
92
- }, null, 2);
93
- }
94
-
95
- function toBackstage(caps, meta) {
96
- const tags = [...new Set(caps.flatMap(c => c.tags))].slice(0, 10);
97
- const lines = [
98
- `apiVersion: backstage.io/v1alpha1`,
99
- `kind: Component`,
100
- `metadata:`,
101
- ` name: ${meta.name}`,
102
- ` description: "${(meta.description || meta.name).replace(/"/g, '\\"')}"`,
103
- ` annotations:`,
104
- ` infernoflow/capability-count: "${caps.length}"`,
105
- ` infernoflow/generated-at: "${new Date().toISOString()}"`,
106
- tags.length ? ` tags:\n${tags.map(t => ` - ${t}`).join("\n")}` : "",
107
- `spec:`,
108
- ` type: service`,
109
- ` lifecycle: production`,
110
- ` owner: team-default`,
111
- ` providesApis: []`,
112
- `---`,
113
- `# Capabilities`,
114
- ...caps.map(c => [
115
- `# ${c.id}`,
116
- `# ${c.description || "(no description)"}`,
117
- `# tags: ${c.tags.join(", ") || "none"}`,
118
- `# status: ${c.status}`,
119
- ].join("\n")),
120
- ].filter(Boolean);
121
- return lines.join("\n") + "\n";
122
- }
123
-
124
- function toCsv(caps) {
125
- const header = ["id", "description", "tags", "status", "since", "owner"];
126
- const rows = caps.map(c => [
127
- c.id,
128
- (c.description || "").replace(/"/g, '""'),
129
- c.tags.join("|"),
130
- c.status,
131
- c.since,
132
- c.owner,
133
- ].map(v => `"${v}"`).join(","));
134
- return [header.join(","), ...rows].join("\n") + "\n";
135
- }
136
-
137
- function toMarkdown(caps, meta) {
138
- const lines = [
139
- `# ${meta.name} — Capability Contract`,
140
- ``,
141
- `> Generated by infernoflow on ${new Date().toISOString().slice(0, 10)}`,
142
- `> ${caps.length} capabilities tracked`,
143
- ``,
144
- `| Capability | Description | Tags | Status |`,
145
- `|---|---|---|---|`,
146
- ...caps.map(c =>
147
- `| \`${c.id}\` | ${c.description || ""} | ${c.tags.join(", ") || "—"} | ${c.status} |`
148
- ),
149
- ``,
150
- ];
151
- return lines.join("\n");
152
- }
153
-
154
- function toCleanJson(caps, meta) {
155
- return JSON.stringify({
156
- name: meta.name,
157
- version: meta.version,
158
- exportedAt: new Date().toISOString(),
159
- capabilities: caps,
160
- }, null, 2) + "\n";
161
- }
162
-
163
- // ── Entry ─────────────────────────────────────────────────────────────────────
164
-
165
- const FORMAT_EXT = {
166
- openapi: "openapi.json",
167
- backstage: "catalog-info.yaml",
168
- csv: "capabilities.csv",
169
- markdown: "capabilities.md",
170
- json: "capabilities-export.json",
171
- };
172
-
173
- export async function exportCommand(rawArgs) {
174
- const args = rawArgs.slice(1);
175
- const jsonMode = args.includes("--json");
176
- const cwd = process.cwd();
177
- const infernoDir = path.join(cwd, "inferno");
178
-
179
- if (!fs.existsSync(infernoDir)) {
180
- const msg = "inferno/ not found. Run: infernoflow init";
181
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
182
- else warn(msg);
183
- process.exit(1);
184
- }
185
-
186
- const fmtIdx = args.indexOf("--format");
187
- const format = fmtIdx !== -1 ? args[fmtIdx + 1] : null;
188
- const outIdx = args.indexOf("--out");
189
- const outArg = outIdx !== -1 ? args[outIdx + 1] : null;
190
-
191
- const validFormats = Object.keys(FORMAT_EXT);
192
-
193
- if (!format || !validFormats.includes(format)) {
194
- const msg = `Usage: infernoflow export --format <${validFormats.join("|")}> [--out <path>]`;
195
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
196
- else {
197
- warn(msg);
198
- console.log();
199
- console.log(` ${gray("Available formats:")}`);
200
- console.log(` ${bold("openapi")} — OpenAPI 3.1 JSON with a stub path per capability`);
201
- console.log(` ${bold("backstage")} — Backstage catalog-info.yaml component definition`);
202
- console.log(` ${bold("csv")} — Spreadsheet-ready CSV`);
203
- console.log(` ${bold("markdown")} — Confluence/Notion table`);
204
- console.log(` ${bold("json")} — Clean normalised JSON`);
205
- console.log();
206
- }
207
- return;
208
- }
209
-
210
- const contract = readContract(infernoDir);
211
- if (!contract) {
212
- const msg = "No contract.json or capabilities.json found.";
213
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
214
- else warn(msg);
215
- process.exit(1);
216
- }
217
-
218
- const caps = normaliseCaps(contract);
219
- const meta = readMeta(infernoDir);
220
- const outPath = outArg || path.join(cwd, FORMAT_EXT[format]);
221
-
222
- let output;
223
- switch (format) {
224
- case "openapi": output = toOpenApi(caps, meta); break;
225
- case "backstage": output = toBackstage(caps, meta); break;
226
- case "csv": output = toCsv(caps); break;
227
- case "markdown": output = toMarkdown(caps, meta); break;
228
- case "json": output = toCleanJson(caps, meta); break;
229
- }
230
-
231
- fs.writeFileSync(outPath, output);
232
-
233
- if (jsonMode) {
234
- console.log(JSON.stringify({ ok: true, format, file: outPath, capabilities: caps.length }));
235
- } else {
236
- done(`Exported ${bold(String(caps.length))} capabilities → ${cyan(path.relative(cwd, outPath))}`);
237
- console.log();
238
- }
239
- }
1
+ import*as d from"node:fs";import*as a from"node:path";import{done as w,warn as u,bold as l,cyan as k,gray as h}from"../ui/output.mjs";function v(t){for(const o of["contract.json","capabilities.json"]){const n=a.join(t,o);if(d.existsSync(n))try{return JSON.parse(d.readFileSync(n,"utf8"))}catch{}}return null}function O(t){return(t?.capabilities||[]).map(n=>typeof n=="string"?{id:n,description:"",tags:[],status:"active"}:{id:n.id||n.name||"unknown",description:n.description||"",tags:n.tags||[],status:n.status||"active",since:n.since||"",owner:n.owner||""})}function x(t){const o=a.join(a.dirname(t),"package.json");try{const n=JSON.parse(d.readFileSync(o,"utf8"));return{name:n.name||"my-service",version:n.version||"0.0.0",description:n.description||""}}catch{return{name:a.basename(a.dirname(t)),version:"0.0.0",description:""}}}function C(t,o){const n={};for(const e of t){const i=`/${e.id.replace(/_/g,"-")}`;n[i]={get:{operationId:e.id,summary:e.description||e.id,tags:e.tags.length?e.tags:[o.name],parameters:[],responses:{200:{description:"Success",content:{"application/json":{schema:{type:"object"}}}},401:{description:"Unauthorized"},500:{description:"Internal Server Error"}}}}}return JSON.stringify({openapi:"3.1.0",info:{title:o.name,version:o.version,description:o.description||`Capability contract for ${o.name}`},paths:n,components:{schemas:{},securitySchemes:{}}},null,2)}function N(t,o){const n=[...new Set(t.flatMap(i=>i.tags))].slice(0,10);return["apiVersion: backstage.io/v1alpha1","kind: Component","metadata:",` name: ${o.name}`,` description: "${(o.description||o.name).replace(/"/g,'\\"')}"`," annotations:",` infernoflow/capability-count: "${t.length}"`,` infernoflow/generated-at: "${new Date().toISOString()}"`,n.length?` tags:
2
+ ${n.map(i=>` - ${i}`).join(`
3
+ `)}`:"","spec:"," type: service"," lifecycle: production"," owner: team-default"," providesApis: []","---","# Capabilities",...t.map(i=>[`# ${i.id}`,`# ${i.description||"(no description)"}`,`# tags: ${i.tags.join(", ")||"none"}`,`# status: ${i.status}`].join(`
4
+ `))].filter(Boolean).join(`
5
+ `)+`
6
+ `}function J(t){const o=["id","description","tags","status","since","owner"],n=t.map(e=>[e.id,(e.description||"").replace(/"/g,'""'),e.tags.join("|"),e.status,e.since,e.owner].map(i=>`"${i}"`).join(","));return[o.join(","),...n].join(`
7
+ `)+`
8
+ `}function I(t,o){return[`# ${o.name} \u2014 Capability Contract`,"",`> Generated by infernoflow on ${new Date().toISOString().slice(0,10)}`,`> ${t.length} capabilities tracked`,"","| Capability | Description | Tags | Status |","|---|---|---|---|",...t.map(e=>`| \`${e.id}\` | ${e.description||""} | ${e.tags.join(", ")||"\u2014"} | ${e.status} |`),""].join(`
9
+ `)}function A(t,o){return JSON.stringify({name:o.name,version:o.version,exportedAt:new Date().toISOString(),capabilities:t},null,2)+`
10
+ `}const S={openapi:"openapi.json",backstage:"catalog-info.yaml",csv:"capabilities.csv",markdown:"capabilities.md",json:"capabilities-export.json"};async function D(t){const o=t.slice(1),n=o.includes("--json"),e=process.cwd(),i=a.join(e,"inferno");if(!d.existsSync(i)){const s="inferno/ not found. Run: infernoflow init";n?console.log(JSON.stringify({ok:!1,error:s})):u(s),process.exit(1)}const m=o.indexOf("--format"),p=m!==-1?o[m+1]:null,b=o.indexOf("--out"),$=b!==-1?o[b+1]:null,y=Object.keys(S);if(!p||!y.includes(p)){const s=`Usage: infernoflow export --format <${y.join("|")}> [--out <path>]`;n?console.log(JSON.stringify({ok:!1,error:s})):(u(s),console.log(),console.log(` ${h("Available formats:")}`),console.log(` ${l("openapi")} \u2014 OpenAPI 3.1 JSON with a stub path per capability`),console.log(` ${l("backstage")} \u2014 Backstage catalog-info.yaml component definition`),console.log(` ${l("csv")} \u2014 Spreadsheet-ready CSV`),console.log(` ${l("markdown")} \u2014 Confluence/Notion table`),console.log(` ${l("json")} \u2014 Clean normalised JSON`),console.log());return}const j=v(i);if(!j){const s="No contract.json or capabilities.json found.";n?console.log(JSON.stringify({ok:!1,error:s})):u(s),process.exit(1)}const r=O(j),f=x(i),g=$||a.join(e,S[p]);let c;switch(p){case"openapi":c=C(r,f);break;case"backstage":c=N(r,f);break;case"csv":c=J(r);break;case"markdown":c=I(r,f);break;case"json":c=A(r,f);break}d.writeFileSync(g,c),n?console.log(JSON.stringify({ok:!0,format:p,file:g,capabilities:r.length})):(w(`Exported ${l(String(r.length))} capabilities \u2192 ${k(a.relative(e,g))}`),console.log())}export{D as exportCommand};
@@ -1,216 +1,12 @@
1
- /**
2
- * infernoflow feedback
3
- *
4
- * Collects in-CLI feedback about infernoflow and optionally opens the web form.
5
- *
6
- * Responses are:
7
- * 1. Saved locally in ~/.infernoflow/feedback.json
8
- * 2. POSTed to Formspree (free, no backend needed — Ron gets email per submission)
9
- *
10
- * To activate Formspree:
11
- * 1. Go to https://formspree.io create free account → New Form
12
- * 2. Replace FORMSPREE_ENDPOINT below with your form URL (e.g. https://formspree.io/f/xabc1234)
13
- * 3. Publish the package — submissions arrive in your email immediately
14
- *
15
- * Usage:
16
- * infernoflow feedback Interactive 5-question survey
17
- * infernoflow feedback --form Open Google Form in browser
18
- * infernoflow feedback --json Print last stored feedback as JSON
19
- */
20
-
21
- import * as fs from "node:fs";
22
- import * as path from "node:path";
23
- import * as os from "node:os";
24
- import * as https from "node:https";
25
- import * as readline from "node:readline";
26
- import { execSync } from "node:child_process";
27
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
28
-
29
- const FORMSPREE_ENDPOINT = "https://formspree.io/f/maqarynd";
30
-
31
- const FEEDBACK_FORM_URL = "https://forms.gle/infernoflow-feedback"; // placeholder — replace with real form
32
- const FEEDBACK_FILE = path.join(os.homedir(), ".infernoflow", "feedback.json");
33
-
34
- const QUESTIONS = [
35
- {
36
- id: "usage",
37
- label: "How often do you use infernoflow?",
38
- choices: ["daily", "a few times a week", "rarely", "just started"],
39
- },
40
- {
41
- id: "ide",
42
- label: "Which IDE are you using?",
43
- choices: ["VS Code + Copilot", "Cursor", "Claude Code", "Windsurf", "Other"],
44
- },
45
- {
46
- id: "top_command",
47
- label: "Which infernoflow command do you use most?",
48
- choices: ["log", "switch", "recap", "status / check", "context", "other"],
49
- },
50
- {
51
- id: "missing",
52
- label: "What feature do you wish infernoflow had?",
53
- freeText: true,
54
- },
55
- {
56
- id: "email",
57
- label: "Email (optional — for follow-up questions):",
58
- freeText: true,
59
- optional: true,
60
- },
61
- ];
62
-
63
- /**
64
- * Fire-and-forget POST to Formspree.
65
- * Formspree emails the form owner on each submission — zero backend needed.
66
- */
67
- function sendToFormspree(record) {
68
- try {
69
- if (!FORMSPREE_ENDPOINT || FORMSPREE_ENDPOINT.includes("placeholder")) return;
70
-
71
- const url = new URL(FORMSPREE_ENDPOINT);
72
- const body = JSON.stringify({
73
- ...record.responses,
74
- _subject: `infernoflow feedback v${record.version}`,
75
- _version: record.version,
76
- _ts: record.ts,
77
- });
78
-
79
- const req = https.request({
80
- hostname: url.hostname,
81
- path: url.pathname,
82
- method: "POST",
83
- headers: {
84
- "Content-Type": "application/json",
85
- "Accept": "application/json",
86
- "Content-Length": Buffer.byteLength(body),
87
- },
88
- timeout: 5000,
89
- });
90
- req.on("error", () => {}); // never surface errors to the user
91
- req.write(body);
92
- req.end();
93
- } catch {}
94
- }
95
-
96
- function saveFeedback(responses) {
97
- const dir = path.dirname(FEEDBACK_FILE);
98
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
99
-
100
- const record = {
101
- ts: new Date().toISOString(),
102
- version: getVersion(),
103
- responses,
104
- };
105
-
106
- // Append to array
107
- let existing = [];
108
- if (fs.existsSync(FEEDBACK_FILE)) {
109
- try { existing = JSON.parse(fs.readFileSync(FEEDBACK_FILE, "utf8")); } catch {}
110
- }
111
- existing.push(record);
112
- fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(existing, null, 2), "utf8");
113
- return record;
114
- }
115
-
116
- function getVersion() {
117
- try {
118
- const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../package.json");
119
- return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
120
- } catch {
121
- return "unknown";
122
- }
123
- }
124
-
125
- function openBrowser(url) {
126
- const platform = process.platform;
127
- try {
128
- if (platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
129
- else if (platform === "win32") execSync(`start "" "${url}"`, { stdio: "ignore" });
130
- else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
131
- return true;
132
- } catch {
133
- return false;
134
- }
135
- }
136
-
137
- async function prompt(rl, question) {
138
- return new Promise(resolve => rl.question(question, resolve));
139
- }
140
-
141
- async function runSurvey() {
142
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
143
-
144
- console.log("\n " + bold("🔥 infernoflow feedback") + "\n");
145
- console.log(gray(" Takes ~60 seconds. Helps make infernoflow better.\n"));
146
-
147
- const responses = {};
148
-
149
- for (const q of QUESTIONS) {
150
- console.log(cyan(` ${q.label}`));
151
-
152
- if (q.choices) {
153
- q.choices.forEach((c, i) => console.log(gray(` ${i + 1}. ${c}`)));
154
- const raw = await prompt(rl, " → ");
155
- const idx = parseInt(raw.trim()) - 1;
156
- responses[q.id] = (idx >= 0 && idx < q.choices.length) ? q.choices[idx] : raw.trim();
157
- } else {
158
- const raw = await prompt(rl, " → ");
159
- responses[q.id] = raw.trim() || (q.optional ? null : "—");
160
- }
161
- console.log();
162
- }
163
-
164
- rl.close();
165
-
166
- const record = saveFeedback(responses);
167
-
168
- // Fire-and-forget cloud send (Formspree — Ron gets an email)
169
- sendToFormspree(record);
170
-
171
- console.log(green(" ✔ Feedback saved — thank you!\n"));
172
- console.log(gray(" Stored in: ~/.infernoflow/feedback.json"));
173
- console.log(gray(` Version: ${record.version}`));
174
-
175
- // Nudge to share
176
- console.log(gray("\n To share more detail or attach files, run: infernoflow feedback --form\n"));
177
- }
178
-
179
- export async function feedbackCommand(args) {
180
- const has = (f) => args.includes(f);
181
-
182
- // ── --form mode ─────────────────────────────────────────────────────────────
183
- if (has("--form")) {
184
- console.log(cyan(`\n Opening feedback form → ${FEEDBACK_FORM_URL}\n`));
185
- const opened = openBrowser(FEEDBACK_FORM_URL);
186
- if (!opened) {
187
- console.log(yellow(" Could not open browser automatically."));
188
- console.log(gray(` Please open manually: ${FEEDBACK_FORM_URL}\n`));
189
- }
190
- return;
191
- }
192
-
193
- // ── --json mode ──────────────────────────────────────────────────────────────
194
- if (has("--json")) {
195
- if (!fs.existsSync(FEEDBACK_FILE)) {
196
- console.log(JSON.stringify([], null, 2));
197
- return;
198
- }
199
- try {
200
- const data = JSON.parse(fs.readFileSync(FEEDBACK_FILE, "utf8"));
201
- console.log(JSON.stringify(data, null, 2));
202
- } catch {
203
- console.log(JSON.stringify([], null, 2));
204
- }
205
- return;
206
- }
207
-
208
- // ── Interactive survey ───────────────────────────────────────────────────────
209
- if (!process.stdin.isTTY) {
210
- console.log(red(" ✘ infernoflow feedback requires an interactive terminal.\n"));
211
- console.log(gray(" Run in a terminal or use: infernoflow feedback --form\n"));
212
- process.exit(1);
213
- }
214
-
215
- await runSurvey();
216
- }
1
+ import*as s from"node:fs";import*as a from"node:path";import*as y from"node:os";import*as w from"node:https";import*as S from"node:readline";import{execSync as f}from"node:child_process";import{bold as b,cyan as p,gray as r,green as k,yellow as O,red as v}from"../ui/output.mjs";const u="https://formspree.io/f/maqarynd",d="https://forms.gle/infernoflow-feedback",i=a.join(y.homedir(),".infernoflow","feedback.json"),x=[{id:"usage",label:"How often do you use infernoflow?",choices:["daily","a few times a week","rarely","just started"]},{id:"ide",label:"Which IDE are you using?",choices:["VS Code + Copilot","Cursor","Claude Code","Windsurf","Other"]},{id:"top_command",label:"Which infernoflow command do you use most?",choices:["log","switch","recap","status / check","context","other"]},{id:"missing",label:"What feature do you wish infernoflow had?",freeText:!0},{id:"email",label:"Email (optional \u2014 for follow-up questions):",freeText:!0,optional:!0}];function E(e){try{if(!u||u.includes("placeholder"))return;const o=new URL(u),t=JSON.stringify({...e.responses,_subject:`infernoflow feedback v${e.version}`,_version:e.version,_ts:e.ts}),n=w.request({hostname:o.hostname,path:o.pathname,method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json","Content-Length":Buffer.byteLength(t)},timeout:5e3});n.on("error",()=>{}),n.write(t),n.end()}catch{}}function F(e){const o=a.dirname(i);s.existsSync(o)||s.mkdirSync(o,{recursive:!0});const t={ts:new Date().toISOString(),version:C(),responses:e};let n=[];if(s.existsSync(i))try{n=JSON.parse(s.readFileSync(i,"utf8"))}catch{}return n.push(t),s.writeFileSync(i,JSON.stringify(n,null,2),"utf8"),t}function C(){try{const e=a.resolve(a.dirname(new URL(import.meta.url).pathname),"../../package.json");return JSON.parse(s.readFileSync(e,"utf8")).version}catch{return"unknown"}}function N(e){const o=process.platform;try{return o==="darwin"?f(`open "${e}"`,{stdio:"ignore"}):o==="win32"?f(`start "" "${e}"`,{stdio:"ignore"}):f(`xdg-open "${e}"`,{stdio:"ignore"}),!0}catch{return!1}}async function m(e,o){return new Promise(t=>e.question(o,t))}async function T(){const e=S.createInterface({input:process.stdin,output:process.stdout});console.log(`
2
+ `+b("\u{1F525} infernoflow feedback")+`
3
+ `),console.log(r(` Takes ~60 seconds. Helps make infernoflow better.
4
+ `));const o={};for(const n of x){if(console.log(p(` ${n.label}`)),n.choices){n.choices.forEach((h,g)=>console.log(r(` ${g+1}. ${h}`)));const c=await m(e," \u2192 "),l=parseInt(c.trim())-1;o[n.id]=l>=0&&l<n.choices.length?n.choices[l]:c.trim()}else{const c=await m(e," \u2192 ");o[n.id]=c.trim()||(n.optional?null:"\u2014")}console.log()}e.close();const t=F(o);E(t),console.log(k(` \u2714 Feedback saved \u2014 thank you!
5
+ `)),console.log(r(" Stored in: ~/.infernoflow/feedback.json")),console.log(r(` Version: ${t.version}`)),console.log(r(`
6
+ To share more detail or attach files, run: infernoflow feedback --form
7
+ `))}async function J(e){const o=t=>e.includes(t);if(o("--form")){console.log(p(`
8
+ Opening feedback form \u2192 ${d}
9
+ `)),N(d)||(console.log(O(" Could not open browser automatically.")),console.log(r(` Please open manually: ${d}
10
+ `)));return}if(o("--json")){if(!s.existsSync(i)){console.log(JSON.stringify([],null,2));return}try{const t=JSON.parse(s.readFileSync(i,"utf8"));console.log(JSON.stringify(t,null,2))}catch{console.log(JSON.stringify([],null,2))}return}process.stdin.isTTY||(console.log(v(` \u2718 infernoflow feedback requires an interactive terminal.
11
+ `)),console.log(r(` Run in a terminal or use: infernoflow feedback --form
12
+ `)),process.exit(1)),await T()}export{J as feedbackCommand};