infernoflow 0.37.1 → 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.
- package/CHANGELOG.md +64 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -520
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,239 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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};
|