infernoflow 0.14.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/infernoflow.mjs +44 -0
- package/dist/lib/commands/changelog.mjs +255 -6
- package/dist/lib/commands/ci.mjs +207 -0
- package/dist/lib/commands/cloud.mjs +521 -0
- package/dist/lib/commands/onboard.mjs +296 -0
- package/dist/lib/commands/share.mjs +236 -0
- package/dist/lib/commands/watch.mjs +203 -0
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow onboard
|
|
3
|
+
*
|
|
4
|
+
* Interactive step-by-step onboarding wizard for new developers.
|
|
5
|
+
* Walks through: what infernoflow is, detecting the stack, running init,
|
|
6
|
+
* showing the first contract, explaining each file, and a test suggest.
|
|
7
|
+
*
|
|
8
|
+
* Designed for teams adding a new member — run once, understand everything.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* infernoflow onboard
|
|
12
|
+
* infernoflow onboard --yes # non-interactive (auto-accept all steps)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as readline from "node:readline";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { header, ok, warn, info, done, bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
21
|
+
|
|
22
|
+
// ── readline helpers ──────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function createRl() {
|
|
25
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ask(rl, question) {
|
|
29
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function confirm(rl, question, defaultYes = true) {
|
|
33
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
34
|
+
const answer = await ask(rl, ` ${question} ${gray(hint)} `);
|
|
35
|
+
if (!answer.trim()) return defaultYes;
|
|
36
|
+
return answer.trim().toLowerCase().startsWith("y");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── step renderer ─────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function step(n, total, title) {
|
|
42
|
+
console.log();
|
|
43
|
+
console.log(` ${bold(cyan(`Step ${n}/${total}`))} ${bold(title)}`);
|
|
44
|
+
console.log(` ${gray("─".repeat(50))}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function explain(lines) {
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
console.log(` ${gray(line)}`);
|
|
50
|
+
}
|
|
51
|
+
console.log();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── project detection ─────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function detectStack(cwd) {
|
|
57
|
+
const has = (f) => fs.existsSync(path.join(cwd, f));
|
|
58
|
+
const pkg = has("package.json") ? JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")) : {};
|
|
59
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
60
|
+
|
|
61
|
+
const framework = deps.react ? "React"
|
|
62
|
+
: deps.next ? "Next.js"
|
|
63
|
+
: deps.vue ? "Vue"
|
|
64
|
+
: deps.angular ? "Angular"
|
|
65
|
+
: deps.express ? "Express"
|
|
66
|
+
: deps.fastify ? "Fastify"
|
|
67
|
+
: has("requirements.txt") ? "Python"
|
|
68
|
+
: has("go.mod") ? "Go"
|
|
69
|
+
: has("Cargo.toml") ? "Rust"
|
|
70
|
+
: "unknown";
|
|
71
|
+
|
|
72
|
+
const language = has("tsconfig.json") ? "TypeScript"
|
|
73
|
+
: has("package.json") ? "JavaScript"
|
|
74
|
+
: has("requirements.txt") ? "Python"
|
|
75
|
+
: has("go.mod") ? "Go"
|
|
76
|
+
: "unknown";
|
|
77
|
+
|
|
78
|
+
return { framework, language, name: pkg.name || path.basename(cwd) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function capture(cmd, cwd) {
|
|
82
|
+
try {
|
|
83
|
+
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── main wizard ───────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export async function onboardCommand(rawArgs) {
|
|
90
|
+
const args = rawArgs.slice(1);
|
|
91
|
+
const autoYes = args.includes("--yes") || args.includes("-y");
|
|
92
|
+
const cwd = process.cwd();
|
|
93
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
94
|
+
const TOTAL = 7;
|
|
95
|
+
|
|
96
|
+
const rl = autoYes ? null : createRl();
|
|
97
|
+
|
|
98
|
+
const ask_confirm = autoYes
|
|
99
|
+
? async () => true
|
|
100
|
+
: (q, def) => confirm(rl, q, def);
|
|
101
|
+
|
|
102
|
+
console.clear();
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(` ${bold("🔥 Welcome to infernoflow")}`);
|
|
105
|
+
console.log(` ${gray("This wizard walks you through everything in about 5 minutes.")}`);
|
|
106
|
+
console.log(` ${gray("You'll understand what infernoflow does and how it fits your workflow.")}`);
|
|
107
|
+
console.log();
|
|
108
|
+
|
|
109
|
+
if (!autoYes) {
|
|
110
|
+
const ready = await ask_confirm("Ready to start?", true);
|
|
111
|
+
if (!ready) { console.log("\n See you next time!\n"); rl.close(); return; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Step 1: What is infernoflow? ───────────────────────────────────────────
|
|
115
|
+
step(1, TOTAL, "What is infernoflow?");
|
|
116
|
+
explain([
|
|
117
|
+
"infernoflow keeps a living record of your project's capabilities",
|
|
118
|
+
"— the features your code actually provides — and makes sure that",
|
|
119
|
+
"record never drifts out of sync as you build.",
|
|
120
|
+
"",
|
|
121
|
+
"It works invisibly in the background:",
|
|
122
|
+
" • Claude auto-tracks capability changes as you code",
|
|
123
|
+
" • Git hooks update the changelog on every commit",
|
|
124
|
+
" • PRs get automatic capability drift analysis",
|
|
125
|
+
" • Version bumps are recommended automatically (major/minor/patch)",
|
|
126
|
+
"",
|
|
127
|
+
"You write code. infernoflow handles the bookkeeping.",
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
await ask_confirm("Got it — continue?", true);
|
|
131
|
+
|
|
132
|
+
// ── Step 2: Detect your project ────────────────────────────────────────────
|
|
133
|
+
step(2, TOTAL, "Detecting your project");
|
|
134
|
+
const stack = detectStack(cwd);
|
|
135
|
+
const isGitRepo = !!capture("git rev-parse --git-dir", cwd);
|
|
136
|
+
const alreadySetup = fs.existsSync(infernoDir);
|
|
137
|
+
|
|
138
|
+
console.log(` Project: ${bold(stack.name)}`);
|
|
139
|
+
console.log(` Framework: ${bold(stack.framework)}`);
|
|
140
|
+
console.log(` Language: ${bold(stack.language)}`);
|
|
141
|
+
console.log(` Git repo: ${isGitRepo ? green("yes") : red("no — git init first")}`);
|
|
142
|
+
console.log(` infernoflow: ${alreadySetup ? green("already set up") : yellow("not set up yet")}`);
|
|
143
|
+
console.log();
|
|
144
|
+
|
|
145
|
+
if (!isGitRepo) {
|
|
146
|
+
warn("This directory is not a git repository.");
|
|
147
|
+
warn("Run: git init && git add . && git commit -m 'init'");
|
|
148
|
+
warn("Then run: infernoflow onboard");
|
|
149
|
+
if (rl) rl.close();
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await ask_confirm("Continue?", true);
|
|
154
|
+
|
|
155
|
+
// ── Step 3: Run infernoflow setup ──────────────────────────────────────────
|
|
156
|
+
step(3, TOTAL, "Setting up infernoflow");
|
|
157
|
+
if (alreadySetup) {
|
|
158
|
+
ok("infernoflow is already set up in this project");
|
|
159
|
+
explain(["The inferno/ folder exists — skipping init."]);
|
|
160
|
+
} else {
|
|
161
|
+
explain([
|
|
162
|
+
"infernoflow setup will:",
|
|
163
|
+
" 1. Scan your codebase and infer capabilities automatically",
|
|
164
|
+
" 2. Create the inferno/ folder with contract.json",
|
|
165
|
+
" 3. Install the MCP server (for Claude Code integration)",
|
|
166
|
+
" 4. Write CLAUDE.md (makes Claude auto-track capabilities)",
|
|
167
|
+
" 5. Install git hooks (auto changelog + drift check)",
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const go = await ask_confirm("Run infernoflow setup now?", true);
|
|
171
|
+
if (go) {
|
|
172
|
+
console.log();
|
|
173
|
+
try {
|
|
174
|
+
execSync("npx infernoflow setup --yes", {
|
|
175
|
+
cwd,
|
|
176
|
+
stdio: "inherit",
|
|
177
|
+
timeout: 120_000,
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
warn("Setup encountered an issue — you can re-run it manually:");
|
|
181
|
+
warn(" infernoflow setup");
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
info("Skipped — run: infernoflow setup");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await ask_confirm("Continue?", true);
|
|
189
|
+
|
|
190
|
+
// ── Step 4: Your capability contract ──────────────────────────────────────
|
|
191
|
+
step(4, TOTAL, "Your capability contract");
|
|
192
|
+
explain([
|
|
193
|
+
"The inferno/contract.json is the heart of infernoflow.",
|
|
194
|
+
"It lists every capability your project has — the things users can DO.",
|
|
195
|
+
"",
|
|
196
|
+
"Think of it as a living API contract, but for features, not endpoints.",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const contractPath = path.join(infernoDir, "contract.json");
|
|
200
|
+
if (fs.existsSync(contractPath)) {
|
|
201
|
+
try {
|
|
202
|
+
const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
203
|
+
const caps = contract.capabilities || [];
|
|
204
|
+
console.log(` ${bold(String(caps.length))} capabilities tracked:\n`);
|
|
205
|
+
for (const c of caps.slice(0, 8)) {
|
|
206
|
+
const cap = typeof c === "string" ? { id: c, title: c } : c;
|
|
207
|
+
console.log(` ${green("✔")} ${bold(cap.id)} ${gray(cap.title || "")}`);
|
|
208
|
+
}
|
|
209
|
+
if (caps.length > 8) console.log(` ${gray(`… and ${caps.length - 8} more`)}`);
|
|
210
|
+
console.log();
|
|
211
|
+
} catch {
|
|
212
|
+
warn("Could not read contract.json");
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
warn("No contract.json yet — run: infernoflow setup");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await ask_confirm("Continue?", true);
|
|
219
|
+
|
|
220
|
+
// ── Step 5: Your daily workflow ────────────────────────────────────────────
|
|
221
|
+
step(5, TOTAL, "Your daily workflow");
|
|
222
|
+
explain([
|
|
223
|
+
"Here's exactly how infernoflow fits into your git workflow:",
|
|
224
|
+
"",
|
|
225
|
+
" 1. git checkout -b feature/my-feature",
|
|
226
|
+
" (branch from main as usual)",
|
|
227
|
+
"",
|
|
228
|
+
" 2. Write code in Claude / Cursor / VS Code",
|
|
229
|
+
" infernoflow tracks capability changes automatically via CLAUDE.md",
|
|
230
|
+
"",
|
|
231
|
+
" 3. git commit -m 'add my feature'",
|
|
232
|
+
" post-commit hook silently updates the changelog",
|
|
233
|
+
"",
|
|
234
|
+
" 4. git push && open PR",
|
|
235
|
+
" GitHub Actions posts a capability drift analysis comment on the PR",
|
|
236
|
+
" pre-push hook warns if drift is HIGH",
|
|
237
|
+
"",
|
|
238
|
+
" 5. infernoflow version",
|
|
239
|
+
" see what semver bump is recommended (major/minor/patch)",
|
|
240
|
+
"",
|
|
241
|
+
"You never run infernoflow manually in day-to-day work.",
|
|
242
|
+
"It runs itself.",
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
await ask_confirm("Got it — continue?", true);
|
|
246
|
+
|
|
247
|
+
// ── Step 6: Key commands to know ──────────────────────────────────────────
|
|
248
|
+
step(6, TOTAL, "Key commands");
|
|
249
|
+
const commands = [
|
|
250
|
+
["infernoflow status", "Quick health check of the contract"],
|
|
251
|
+
["infernoflow diff", "What changed since the last release"],
|
|
252
|
+
["infernoflow version", "What semver bump to use"],
|
|
253
|
+
["infernoflow version --apply", "Write the bump to package.json"],
|
|
254
|
+
["infernoflow changelog ai", "Generate a human-readable changelog"],
|
|
255
|
+
["infernoflow dashboard", "Open the live web dashboard"],
|
|
256
|
+
["infernoflow team-sync status","See if your team is in sync"],
|
|
257
|
+
["infernoflow suggest 'what I built'", "Manually update the contract"],
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
const maxLen = Math.max(...commands.map(([c]) => c.length));
|
|
261
|
+
for (const [cmd, desc] of commands) {
|
|
262
|
+
console.log(` ${cyan(cmd.padEnd(maxLen + 2))}${gray(desc)}`);
|
|
263
|
+
}
|
|
264
|
+
console.log();
|
|
265
|
+
|
|
266
|
+
await ask_confirm("Continue?", true);
|
|
267
|
+
|
|
268
|
+
// ── Step 7: Live test ─────────────────────────────────────────────────────
|
|
269
|
+
step(7, TOTAL, "Quick live test");
|
|
270
|
+
explain(["Let's run infernoflow status to confirm everything is working."]);
|
|
271
|
+
|
|
272
|
+
const runTest = await ask_confirm("Run infernoflow status now?", true);
|
|
273
|
+
if (runTest) {
|
|
274
|
+
console.log();
|
|
275
|
+
try {
|
|
276
|
+
execSync("npx infernoflow status", { cwd, stdio: "inherit", timeout: 30_000 });
|
|
277
|
+
} catch {
|
|
278
|
+
warn("Status check failed — try: infernoflow setup");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
283
|
+
console.log();
|
|
284
|
+
console.log(` ${bold("🎉 You're all set!")}`);
|
|
285
|
+
console.log();
|
|
286
|
+
console.log(` ${green("✔")} infernoflow is installed and running`);
|
|
287
|
+
console.log(` ${green("✔")} Claude will auto-track capabilities as you code`);
|
|
288
|
+
console.log(` ${green("✔")} Git hooks handle changelog and drift automatically`);
|
|
289
|
+
console.log(` ${green("✔")} PRs will get automatic capability analysis`);
|
|
290
|
+
console.log();
|
|
291
|
+
console.log(` ${gray("Share this with teammates:")} ${cyan("infernoflow onboard")}`);
|
|
292
|
+
console.log(` ${gray("Questions?")} ${cyan("infernoflow --help")}`);
|
|
293
|
+
console.log();
|
|
294
|
+
|
|
295
|
+
if (rl) rl.close();
|
|
296
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow share
|
|
3
|
+
*
|
|
4
|
+
* Generate a public read-only snapshot of your capability contract — no cloud
|
|
5
|
+
* account needed. Creates a self-contained HTML file you can host anywhere,
|
|
6
|
+
* or uploads to a paste service and prints a short link.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* infernoflow share Generate share.html locally
|
|
10
|
+
* infernoflow share --open Open in browser immediately
|
|
11
|
+
* infernoflow share --upload Upload to dpaste.com and print URL
|
|
12
|
+
* infernoflow share --copy Copy HTML to clipboard
|
|
13
|
+
* infernoflow share --json Machine-readable: { ok, file, url }
|
|
14
|
+
* infernoflow share --out <path> Custom output path
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import * as https from "node:https";
|
|
20
|
+
import { execSync } from "node:child_process";
|
|
21
|
+
import { done, warn, info, bold, cyan, gray, green } from "../ui/output.mjs";
|
|
22
|
+
|
|
23
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function readContract(infernoDir) {
|
|
26
|
+
for (const f of ["contract.json", "capabilities.json"]) {
|
|
27
|
+
const p = path.join(infernoDir, f);
|
|
28
|
+
if (fs.existsSync(p)) {
|
|
29
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readChangelog(cwd) {
|
|
36
|
+
const p = path.join(cwd, "CHANGELOG.md");
|
|
37
|
+
if (!fs.existsSync(p)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const lines = fs.readFileSync(p, "utf8").split("\n");
|
|
40
|
+
const start = lines.findIndex(l => l.startsWith("## "));
|
|
41
|
+
if (start === -1) return null;
|
|
42
|
+
const end = lines.findIndex((l, i) => i > start && l.startsWith("## "));
|
|
43
|
+
return lines.slice(start, end === -1 ? start + 30 : end).join("\n");
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── HTML generator ────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function buildHtml(contract, changelog, generatedAt) {
|
|
50
|
+
const caps = contract.capabilities || [];
|
|
51
|
+
const policyId = contract.policyId || "project";
|
|
52
|
+
const version = contract.policyVersion || "?";
|
|
53
|
+
|
|
54
|
+
const capRows = caps.map(c => {
|
|
55
|
+
const id = typeof c === "string" ? c : c.id;
|
|
56
|
+
const title = typeof c === "string" ? c : (c.title || c.id);
|
|
57
|
+
const cov = typeof c === "object" ? c.covered : undefined;
|
|
58
|
+
const badge = cov === true ? `<span class="cov yes">✔</span>`
|
|
59
|
+
: cov === false ? `<span class="cov no">✗</span>`
|
|
60
|
+
: `<span class="cov uk">·</span>`;
|
|
61
|
+
const desc = typeof c === "object" && c.description ? `<div class="cap-desc">${c.description}</div>` : "";
|
|
62
|
+
return `<div class="cap-row">${badge}<div class="cap-body"><div class="cap-id">${id}</div><div class="cap-title">${title !== id ? title : ""}</div>${desc}</div></div>`;
|
|
63
|
+
}).join("");
|
|
64
|
+
|
|
65
|
+
const changelogHtml = changelog
|
|
66
|
+
? `<section><h2>Latest changes</h2><pre class="changelog">${changelog.replace(/</g, "<")}</pre></section>`
|
|
67
|
+
: "";
|
|
68
|
+
|
|
69
|
+
return `<!doctype html>
|
|
70
|
+
<html lang="en">
|
|
71
|
+
<head>
|
|
72
|
+
<meta charset="UTF-8">
|
|
73
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
74
|
+
<title>${policyId} v${version} — infernoflow snapshot</title>
|
|
75
|
+
<style>
|
|
76
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
77
|
+
body{background:#0f0f1a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;line-height:1.5;padding:0 0 3rem}
|
|
78
|
+
header{background:#1a1a2e;border-bottom:2px solid #f97316;padding:1.5rem 2rem}
|
|
79
|
+
header h1{font-size:1.5rem;font-weight:700;color:#f97316}
|
|
80
|
+
header .meta{color:#64748b;font-size:0.85rem;margin-top:0.25rem}
|
|
81
|
+
main{max-width:860px;margin:2rem auto;padding:0 1.5rem}
|
|
82
|
+
section{margin-bottom:2rem}
|
|
83
|
+
h2{font-size:0.8rem;text-transform:uppercase;letter-spacing:0.08em;color:#64748b;margin-bottom:0.75rem;padding-bottom:0.4rem;border-bottom:1px solid #2d2d4e}
|
|
84
|
+
.stats{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}
|
|
85
|
+
.stat{background:#1a1a2e;border:1px solid #2d2d4e;border-radius:8px;padding:1rem 1.5rem;min-width:130px}
|
|
86
|
+
.stat .n{font-size:2rem;font-weight:700;color:#f97316}
|
|
87
|
+
.stat .l{font-size:0.75rem;color:#64748b;margin-top:2px}
|
|
88
|
+
.cap-row{display:flex;align-items:flex-start;gap:0.75rem;padding:0.6rem 0;border-bottom:1px solid #1e1e30}
|
|
89
|
+
.cap-row:last-child{border-bottom:none}
|
|
90
|
+
.cov{font-size:0.9rem;min-width:18px;text-align:center;margin-top:2px}
|
|
91
|
+
.cov.yes{color:#4ade80}.cov.no{color:#f87171}.cov.uk{color:#475569}
|
|
92
|
+
.cap-id{font-weight:600;font-size:0.9rem}
|
|
93
|
+
.cap-title{color:#94a3b8;font-size:0.8rem}
|
|
94
|
+
.cap-desc{color:#64748b;font-size:0.78rem;margin-top:2px}
|
|
95
|
+
.changelog{background:#1a1a2e;border:1px solid #2d2d4e;border-radius:6px;padding:1rem;font-size:0.8rem;white-space:pre-wrap;color:#94a3b8;overflow-x:auto}
|
|
96
|
+
footer{text-align:center;color:#334155;font-size:0.75rem;margin-top:3rem}
|
|
97
|
+
footer a{color:#f97316;text-decoration:none}
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<header>
|
|
102
|
+
<h1>🔥 ${policyId}</h1>
|
|
103
|
+
<div class="meta">v${version} · ${caps.length} capabilities · snapshot ${generatedAt}</div>
|
|
104
|
+
</header>
|
|
105
|
+
<main>
|
|
106
|
+
<div class="stats">
|
|
107
|
+
<div class="stat"><div class="n">${caps.length}</div><div class="l">capabilities</div></div>
|
|
108
|
+
<div class="stat"><div class="n">${caps.filter(c => typeof c === "object" && c.covered).length || "—"}</div><div class="l">covered</div></div>
|
|
109
|
+
<div class="stat"><div class="n">v${version}</div><div class="l">version</div></div>
|
|
110
|
+
</div>
|
|
111
|
+
<section>
|
|
112
|
+
<h2>Capabilities</h2>
|
|
113
|
+
<div>${capRows || '<p style="color:#475569">No capabilities yet.</p>'}</div>
|
|
114
|
+
</section>
|
|
115
|
+
${changelogHtml}
|
|
116
|
+
</main>
|
|
117
|
+
<footer>Generated by <a href="https://github.com/ronmiz/infernoflow">infernoflow</a> on ${generatedAt}</footer>
|
|
118
|
+
</body>
|
|
119
|
+
</html>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Upload to dpaste ──────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function uploadToDpaste(content) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const body = `content=${encodeURIComponent(content)}&syntax=html&expiry_days=365`;
|
|
127
|
+
const req = https.request({
|
|
128
|
+
hostname: "dpaste.com",
|
|
129
|
+
path: "/api/v2/",
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
133
|
+
"Content-Length": Buffer.byteLength(body),
|
|
134
|
+
"User-Agent": "infernoflow-cli",
|
|
135
|
+
},
|
|
136
|
+
}, (res) => {
|
|
137
|
+
let data = "";
|
|
138
|
+
res.on("data", d => (data += d));
|
|
139
|
+
res.on("end", () => {
|
|
140
|
+
const url = data.trim();
|
|
141
|
+
if (url.startsWith("http")) resolve(url + ".html");
|
|
142
|
+
else reject(new Error("Unexpected response: " + data));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
req.on("error", reject);
|
|
146
|
+
req.write(body);
|
|
147
|
+
req.end();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export async function shareCommand(rawArgs) {
|
|
154
|
+
const args = rawArgs.slice(1);
|
|
155
|
+
const jsonMode = args.includes("--json");
|
|
156
|
+
const openBrowser = args.includes("--open");
|
|
157
|
+
const upload = args.includes("--upload");
|
|
158
|
+
const copyToClip = args.includes("--copy");
|
|
159
|
+
const outIdx = args.indexOf("--out");
|
|
160
|
+
const cwd = process.cwd();
|
|
161
|
+
const infernoDir = path.join(cwd, "inferno");
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(infernoDir)) {
|
|
164
|
+
const msg = "inferno/ not found. Run: infernoflow init";
|
|
165
|
+
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const contract = readContract(infernoDir);
|
|
170
|
+
if (!contract) {
|
|
171
|
+
const msg = "No contract.json found. Run: infernoflow init";
|
|
172
|
+
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const changelog = readChangelog(cwd);
|
|
177
|
+
const generatedAt = new Date().toLocaleString();
|
|
178
|
+
const htmlContent = buildHtml(contract, changelog, generatedAt);
|
|
179
|
+
|
|
180
|
+
const outPath = outIdx !== -1 ? args[outIdx + 1] : path.join(cwd, "inferno", "share.html");
|
|
181
|
+
fs.writeFileSync(outPath, htmlContent, "utf8");
|
|
182
|
+
|
|
183
|
+
let url = null;
|
|
184
|
+
|
|
185
|
+
if (upload) {
|
|
186
|
+
if (!jsonMode) info("Uploading to dpaste.com…");
|
|
187
|
+
try {
|
|
188
|
+
url = await uploadToDpaste(htmlContent);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (!jsonMode) warn(`Upload failed: ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (copyToClip) {
|
|
195
|
+
try {
|
|
196
|
+
const cmd = process.platform === "win32" ? `echo ${JSON.stringify(htmlContent)} | clip`
|
|
197
|
+
: process.platform === "darwin" ? `pbcopy`
|
|
198
|
+
: `xclip -selection clipboard`;
|
|
199
|
+
if (process.platform === "darwin") {
|
|
200
|
+
const { execSync: ex } = await import("node:child_process");
|
|
201
|
+
const proc = ex;
|
|
202
|
+
require("child_process").execSync("pbcopy", { input: htmlContent });
|
|
203
|
+
} else {
|
|
204
|
+
execSync(cmd, { input: htmlContent });
|
|
205
|
+
}
|
|
206
|
+
if (!jsonMode) info("HTML copied to clipboard");
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (openBrowser) {
|
|
211
|
+
try {
|
|
212
|
+
const target = url || outPath;
|
|
213
|
+
const cmd = process.platform === "win32" ? `start "" "${target}"`
|
|
214
|
+
: process.platform === "darwin" ? `open "${target}"`
|
|
215
|
+
: `xdg-open "${target}"`;
|
|
216
|
+
execSync(cmd, { stdio: "ignore" });
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (jsonMode) {
|
|
221
|
+
console.log(JSON.stringify({ ok: true, file: outPath, url }));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const caps = (contract.capabilities || []).length;
|
|
226
|
+
done(`Snapshot created — ${bold(String(caps))} capabilities`);
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(` File: ${cyan(outPath)}`);
|
|
229
|
+
if (url) console.log(` URL: ${cyan(url)}`);
|
|
230
|
+
console.log();
|
|
231
|
+
if (!url) {
|
|
232
|
+
console.log(` ${gray("Share the file or run with --upload to get a public URL:")}`);
|
|
233
|
+
console.log(` ${cyan("infernoflow share --upload --open")}`);
|
|
234
|
+
}
|
|
235
|
+
console.log();
|
|
236
|
+
}
|