infernoflow 0.32.7 → 0.32.9
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 +84 -255
- 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/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 +25 -130
- package/dist/lib/commands/cloud.mjs +5 -521
- package/dist/lib/commands/context.mjs +31 -287
- 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/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +203 -320
- 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 +23 -475
- 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/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/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 +5 -558
- 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/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- 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/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/templates/index.mjs +1 -131
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -72
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
- package/dist/templates/github-app/GITHUB_APP.md +67 -0
- package/dist/templates/github-app/app-manifest.json +20 -0
- package/package.json +1 -1
|
@@ -1,521 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
* Sub-commands:
|
|
8
|
-
* cloud init Generate a project token and write inferno/.cloud.json
|
|
9
|
-
* cloud push Upload local contract to cloud
|
|
10
|
-
* cloud pull Download latest contract from cloud
|
|
11
|
-
* cloud status Show local vs cloud diff
|
|
12
|
-
* cloud dashboard Print hosted dashboard URL
|
|
13
|
-
*
|
|
14
|
-
* Flags:
|
|
15
|
-
* --token <tok> Override token from env INFERNOFLOW_TOKEN
|
|
16
|
-
* --endpoint <url> Override default endpoint
|
|
17
|
-
* --dry-run Print what would happen without sending
|
|
18
|
-
* --json Machine-readable output
|
|
19
|
-
*
|
|
20
|
-
* Usage:
|
|
21
|
-
* infernoflow cloud init
|
|
22
|
-
* infernoflow cloud push
|
|
23
|
-
* infernoflow cloud pull
|
|
24
|
-
* infernoflow cloud status --json
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import * as fs from "node:fs";
|
|
28
|
-
import * as path from "node:path";
|
|
29
|
-
import * as https from "node:https";
|
|
30
|
-
import * as http from "node:http";
|
|
31
|
-
import * as crypto from "node:crypto";
|
|
32
|
-
import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
|
|
33
|
-
|
|
34
|
-
// ── Config ────────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const DEFAULT_ENDPOINT = "https://cloud.infernoflow.dev";
|
|
37
|
-
const CLOUD_CONFIG_FILE = ".cloud.json";
|
|
38
|
-
|
|
39
|
-
function readCloudConfig(infernoDir) {
|
|
40
|
-
const p = path.join(infernoDir, CLOUD_CONFIG_FILE);
|
|
41
|
-
if (!fs.existsSync(p)) return null;
|
|
42
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function writeCloudConfig(infernoDir, config) {
|
|
46
|
-
const p = path.join(infernoDir, CLOUD_CONFIG_FILE);
|
|
47
|
-
fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getToken(config, args) {
|
|
51
|
-
const idx = args.indexOf("--token");
|
|
52
|
-
if (idx !== -1) return args[idx + 1];
|
|
53
|
-
return process.env.INFERNOFLOW_TOKEN || config?.token || null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getEndpoint(config, args) {
|
|
57
|
-
const idx = args.indexOf("--endpoint");
|
|
58
|
-
if (idx !== -1) return args[idx + 1];
|
|
59
|
-
return process.env.INFERNOFLOW_ENDPOINT || config?.endpoint || DEFAULT_ENDPOINT;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
function httpsRequest(method, url, body, token) {
|
|
65
|
-
return new Promise((resolve, reject) => {
|
|
66
|
-
const parsed = new URL(url);
|
|
67
|
-
const isHttps = parsed.protocol === "https:";
|
|
68
|
-
const lib = isHttps ? https : http;
|
|
69
|
-
const payload = body ? JSON.stringify(body) : null;
|
|
70
|
-
|
|
71
|
-
const options = {
|
|
72
|
-
hostname: parsed.hostname,
|
|
73
|
-
port: parsed.port || (isHttps ? 443 : 80),
|
|
74
|
-
path: parsed.pathname + (parsed.search || ""),
|
|
75
|
-
method,
|
|
76
|
-
headers: {
|
|
77
|
-
"Content-Type": "application/json",
|
|
78
|
-
"Accept": "application/json",
|
|
79
|
-
"User-Agent": "infernoflow-cli",
|
|
80
|
-
...(token ? { "Authorization": `Bearer ${token}` } : {}),
|
|
81
|
-
...(payload ? { "Content-Length": Buffer.byteLength(payload) } : {}),
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const req = lib.request(options, (res) => {
|
|
86
|
-
let data = "";
|
|
87
|
-
res.on("data", (chunk) => (data += chunk));
|
|
88
|
-
res.on("end", () => {
|
|
89
|
-
try {
|
|
90
|
-
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
91
|
-
} catch {
|
|
92
|
-
resolve({ status: res.statusCode, body: data });
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
req.on("error", reject);
|
|
98
|
-
if (payload) req.write(payload);
|
|
99
|
-
req.end();
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Contract helpers ──────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
function readContract(infernoDir) {
|
|
106
|
-
const candidates = ["contract.json", "capabilities.json"];
|
|
107
|
-
for (const f of candidates) {
|
|
108
|
-
const p = path.join(infernoDir, f);
|
|
109
|
-
if (fs.existsSync(p)) {
|
|
110
|
-
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function contractHash(contract) {
|
|
117
|
-
return crypto.createHash("sha256").update(JSON.stringify(contract)).digest("hex").slice(0, 12);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ── Sub-commands ──────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
async function subcmdInit(args, cwd, infernoDir) {
|
|
123
|
-
const jsonMode = args.includes("--json");
|
|
124
|
-
const endpoint = getEndpoint(null, args);
|
|
125
|
-
const dryRun = args.includes("--dry-run");
|
|
126
|
-
|
|
127
|
-
// Check for existing config
|
|
128
|
-
const existing = readCloudConfig(infernoDir);
|
|
129
|
-
if (existing && !args.includes("--force") && !args.includes("-f")) {
|
|
130
|
-
if (jsonMode) {
|
|
131
|
-
console.log(JSON.stringify({ ok: false, error: "Already initialised. Use --force to overwrite.", config: existing }));
|
|
132
|
-
} else {
|
|
133
|
-
warn("Cloud already configured for this project.");
|
|
134
|
-
console.log(` Token: ${gray(existing.token)}`);
|
|
135
|
-
console.log(` Endpoint: ${gray(existing.endpoint)}`);
|
|
136
|
-
console.log(` Project: ${gray(existing.projectId)}`);
|
|
137
|
-
console.log();
|
|
138
|
-
info("Use --force to generate a new token.");
|
|
139
|
-
}
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Generate a project ID and token
|
|
144
|
-
const projectId = crypto.randomBytes(8).toString("hex");
|
|
145
|
-
const token = crypto.randomBytes(24).toString("base64url");
|
|
146
|
-
|
|
147
|
-
const config = {
|
|
148
|
-
projectId,
|
|
149
|
-
token,
|
|
150
|
-
endpoint,
|
|
151
|
-
createdAt: new Date().toISOString(),
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
if (dryRun) {
|
|
155
|
-
if (jsonMode) {
|
|
156
|
-
console.log(JSON.stringify({ ok: true, dryRun: true, config }));
|
|
157
|
-
} else {
|
|
158
|
-
info("Dry run — would write inferno/.cloud.json:");
|
|
159
|
-
console.log(" " + JSON.stringify(config, null, 2).split("\n").join("\n "));
|
|
160
|
-
}
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!jsonMode) header("Initialising infernoflow cloud");
|
|
165
|
-
|
|
166
|
-
// Register project with cloud endpoint (best-effort)
|
|
167
|
-
try {
|
|
168
|
-
const resp = await httpsRequest("POST", `${endpoint}/api/projects`, { projectId }, null);
|
|
169
|
-
if (resp.status === 200 || resp.status === 201) {
|
|
170
|
-
if (!jsonMode) ok("Project registered on cloud");
|
|
171
|
-
}
|
|
172
|
-
} catch {
|
|
173
|
-
if (!jsonMode) info("Cloud endpoint unreachable — saved config locally (will connect on first push)");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
writeCloudConfig(infernoDir, config);
|
|
177
|
-
|
|
178
|
-
if (jsonMode) {
|
|
179
|
-
console.log(JSON.stringify({ ok: true, projectId, endpoint }));
|
|
180
|
-
} else {
|
|
181
|
-
done("Cloud configured!");
|
|
182
|
-
console.log();
|
|
183
|
-
console.log(` Project ID: ${cyan(projectId)}`);
|
|
184
|
-
console.log(` Endpoint: ${gray(endpoint)}`);
|
|
185
|
-
console.log(` Token: ${gray(token.slice(0, 8) + "…")} (stored in inferno/.cloud.json)`);
|
|
186
|
-
console.log();
|
|
187
|
-
console.log(` ${gray("Share the dashboard:")} ${cyan(`${endpoint}/p/${projectId}`)}`);
|
|
188
|
-
console.log();
|
|
189
|
-
console.log(` ${yellow("⚠")} Add inferno/.cloud.json to .gitignore to protect your token!`);
|
|
190
|
-
console.log(` ${gray("echo 'inferno/.cloud.json' >> .gitignore")}`);
|
|
191
|
-
console.log();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function subcmdPush(args, cwd, infernoDir) {
|
|
196
|
-
const jsonMode = args.includes("--json");
|
|
197
|
-
const dryRun = args.includes("--dry-run");
|
|
198
|
-
const config = readCloudConfig(infernoDir);
|
|
199
|
-
const token = getToken(config, args);
|
|
200
|
-
const endpoint = getEndpoint(config, args);
|
|
201
|
-
|
|
202
|
-
if (!token) {
|
|
203
|
-
const msg = "No token found. Run: infernoflow cloud init";
|
|
204
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const contract = readContract(infernoDir);
|
|
209
|
-
if (!contract) {
|
|
210
|
-
const msg = "No contract.json found. Run: infernoflow init";
|
|
211
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const projectId = config?.projectId || "unknown";
|
|
216
|
-
const hash = contractHash(contract);
|
|
217
|
-
const caps = (contract.capabilities || []).length;
|
|
218
|
-
|
|
219
|
-
if (dryRun) {
|
|
220
|
-
if (jsonMode) {
|
|
221
|
-
console.log(JSON.stringify({ ok: true, dryRun: true, projectId, hash, capabilities: caps }));
|
|
222
|
-
} else {
|
|
223
|
-
info(`Dry run — would push ${bold(String(caps))} capabilities (hash: ${hash}) to ${endpoint}`);
|
|
224
|
-
}
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!jsonMode) header("Pushing contract to cloud");
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const resp = await httpsRequest(
|
|
232
|
-
"PUT",
|
|
233
|
-
`${endpoint}/api/projects/${projectId}/contract`,
|
|
234
|
-
{ contract, hash, pushedAt: new Date().toISOString() },
|
|
235
|
-
token
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
if (resp.status === 200 || resp.status === 201 || resp.status === 204) {
|
|
239
|
-
if (jsonMode) {
|
|
240
|
-
console.log(JSON.stringify({ ok: true, projectId, hash, capabilities: caps }));
|
|
241
|
-
} else {
|
|
242
|
-
done(`Pushed ${bold(String(caps))} capabilities`);
|
|
243
|
-
console.log(` ${gray("Dashboard:")} ${cyan(`${endpoint}/p/${projectId}`)}`);
|
|
244
|
-
console.log();
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
const errMsg = `Cloud returned ${resp.status}`;
|
|
248
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: errMsg, status: resp.status })); }
|
|
249
|
-
else { warn(errMsg); }
|
|
250
|
-
process.exit(1);
|
|
251
|
-
}
|
|
252
|
-
} catch (err) {
|
|
253
|
-
// Cloud unreachable — save a pending push marker
|
|
254
|
-
const pendingPath = path.join(infernoDir, ".cloud-pending.json");
|
|
255
|
-
fs.writeFileSync(pendingPath, JSON.stringify({ hash, pendingAt: new Date().toISOString() }));
|
|
256
|
-
|
|
257
|
-
if (jsonMode) {
|
|
258
|
-
console.log(JSON.stringify({ ok: false, error: err.message, pending: true }));
|
|
259
|
-
} else {
|
|
260
|
-
warn("Cloud unreachable — push queued locally.");
|
|
261
|
-
info("Changes will sync automatically on next successful connection.");
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function subcmdPull(args, cwd, infernoDir) {
|
|
267
|
-
const jsonMode = args.includes("--json");
|
|
268
|
-
const dryRun = args.includes("--dry-run");
|
|
269
|
-
const config = readCloudConfig(infernoDir);
|
|
270
|
-
const token = getToken(config, args);
|
|
271
|
-
const endpoint = getEndpoint(config, args);
|
|
272
|
-
|
|
273
|
-
if (!token) {
|
|
274
|
-
const msg = "No token found. Run: infernoflow cloud init";
|
|
275
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
|
|
276
|
-
process.exit(1);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const projectId = config?.projectId || "unknown";
|
|
280
|
-
|
|
281
|
-
if (!jsonMode) header("Pulling contract from cloud");
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const resp = await httpsRequest(
|
|
285
|
-
"GET",
|
|
286
|
-
`${endpoint}/api/projects/${projectId}/contract`,
|
|
287
|
-
null,
|
|
288
|
-
token
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
if (resp.status !== 200) {
|
|
292
|
-
const errMsg = `Cloud returned ${resp.status}`;
|
|
293
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: errMsg })); }
|
|
294
|
-
else { warn(errMsg); }
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const remote = resp.body?.contract;
|
|
299
|
-
const localRaw = readContract(infernoDir);
|
|
300
|
-
|
|
301
|
-
if (!remote) {
|
|
302
|
-
const msg = "No contract found on cloud. Push first.";
|
|
303
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
|
|
304
|
-
else { warn(msg); }
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Detect conflicts (same capability changed on both sides)
|
|
309
|
-
const localCaps = (localRaw?.capabilities || []).map(c => typeof c === "string" ? c : c.id);
|
|
310
|
-
const remoteCaps = (remote.capabilities || []).map(c => typeof c === "string" ? c : c.id);
|
|
311
|
-
const localSet = new Set(localCaps);
|
|
312
|
-
const remoteSet = new Set(remoteCaps);
|
|
313
|
-
const onlyLocal = localCaps.filter(id => !remoteSet.has(id));
|
|
314
|
-
const onlyRemote = remoteCaps.filter(id => !localSet.has(id));
|
|
315
|
-
|
|
316
|
-
if (onlyLocal.length > 0 && onlyRemote.length > 0) {
|
|
317
|
-
if (!jsonMode) {
|
|
318
|
-
warn("Diverged contracts detected:");
|
|
319
|
-
onlyLocal.forEach(id => console.log(` ${red("-")} local-only: ${id}`));
|
|
320
|
-
onlyRemote.forEach(id => console.log(` ${green("+")} remote-only: ${id}`));
|
|
321
|
-
console.log();
|
|
322
|
-
warn("Merge manually or use --force to overwrite local with remote.");
|
|
323
|
-
} else {
|
|
324
|
-
console.log(JSON.stringify({
|
|
325
|
-
ok: false,
|
|
326
|
-
conflict: true,
|
|
327
|
-
onlyLocal,
|
|
328
|
-
onlyRemote,
|
|
329
|
-
}));
|
|
330
|
-
}
|
|
331
|
-
if (!args.includes("--force") && !args.includes("-f")) return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (dryRun) {
|
|
335
|
-
if (jsonMode) {
|
|
336
|
-
console.log(JSON.stringify({ ok: true, dryRun: true, capabilities: remoteCaps.length, hash: contractHash(remote) }));
|
|
337
|
-
} else {
|
|
338
|
-
info(`Dry run — would write ${bold(String(remoteCaps.length))} capabilities from cloud`);
|
|
339
|
-
}
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Write pulled contract
|
|
344
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
345
|
-
fs.writeFileSync(contractPath, JSON.stringify(remote, null, 2) + "\n");
|
|
346
|
-
|
|
347
|
-
if (jsonMode) {
|
|
348
|
-
console.log(JSON.stringify({ ok: true, capabilities: remoteCaps.length, hash: contractHash(remote) }));
|
|
349
|
-
} else {
|
|
350
|
-
done(`Pulled ${bold(String(remoteCaps.length))} capabilities from cloud`);
|
|
351
|
-
if (onlyLocal.length) warn(`${onlyLocal.length} local-only capabilities were overwritten.`);
|
|
352
|
-
console.log();
|
|
353
|
-
}
|
|
354
|
-
} catch (err) {
|
|
355
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: err.message })); }
|
|
356
|
-
else { warn(`Cloud unreachable: ${err.message}`); }
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function subcmdStatus(args, cwd, infernoDir) {
|
|
362
|
-
const jsonMode = args.includes("--json");
|
|
363
|
-
const config = readCloudConfig(infernoDir);
|
|
364
|
-
const token = getToken(config, args);
|
|
365
|
-
const endpoint = getEndpoint(config, args);
|
|
366
|
-
|
|
367
|
-
if (!config) {
|
|
368
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "Not initialised. Run: infernoflow cloud init" })); }
|
|
369
|
-
else { warn("Cloud not configured. Run: infernoflow cloud init"); }
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const projectId = config.projectId;
|
|
374
|
-
const localContract = readContract(infernoDir);
|
|
375
|
-
const localHash = localContract ? contractHash(localContract) : null;
|
|
376
|
-
const localCaps = (localContract?.capabilities || []).length;
|
|
377
|
-
|
|
378
|
-
if (!jsonMode) header("Cloud status");
|
|
379
|
-
|
|
380
|
-
let remoteHash = null;
|
|
381
|
-
let remoteCaps = 0;
|
|
382
|
-
let reachable = false;
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
const resp = await httpsRequest(
|
|
386
|
-
"GET",
|
|
387
|
-
`${endpoint}/api/projects/${projectId}/contract`,
|
|
388
|
-
null,
|
|
389
|
-
token
|
|
390
|
-
);
|
|
391
|
-
if (resp.status === 200 && resp.body?.contract) {
|
|
392
|
-
reachable = true;
|
|
393
|
-
remoteHash = contractHash(resp.body.contract);
|
|
394
|
-
remoteCaps = (resp.body.contract?.capabilities || []).length;
|
|
395
|
-
}
|
|
396
|
-
} catch {}
|
|
397
|
-
|
|
398
|
-
const inSync = localHash === remoteHash;
|
|
399
|
-
const pending = fs.existsSync(path.join(infernoDir, ".cloud-pending.json"));
|
|
400
|
-
|
|
401
|
-
if (jsonMode) {
|
|
402
|
-
console.log(JSON.stringify({
|
|
403
|
-
ok: true,
|
|
404
|
-
projectId,
|
|
405
|
-
endpoint,
|
|
406
|
-
reachable,
|
|
407
|
-
inSync,
|
|
408
|
-
pending,
|
|
409
|
-
local: { hash: localHash, capabilities: localCaps },
|
|
410
|
-
remote: reachable ? { hash: remoteHash, capabilities: remoteCaps } : null,
|
|
411
|
-
}));
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
console.log(` Project: ${cyan(projectId)}`);
|
|
416
|
-
console.log(` Endpoint: ${gray(endpoint)}`);
|
|
417
|
-
console.log(` Dashboard: ${cyan(`${endpoint}/p/${projectId}`)}`);
|
|
418
|
-
console.log();
|
|
419
|
-
console.log(` Local: ${bold(String(localCaps))} capabilities ${gray("(hash: " + (localHash || "none") + ")")}`);
|
|
420
|
-
|
|
421
|
-
if (!reachable) {
|
|
422
|
-
console.log(` Cloud: ${yellow("unreachable")}`);
|
|
423
|
-
} else {
|
|
424
|
-
console.log(` Cloud: ${bold(String(remoteCaps))} capabilities ${gray("(hash: " + (remoteHash || "none") + ")")}`);
|
|
425
|
-
console.log();
|
|
426
|
-
if (inSync) {
|
|
427
|
-
console.log(` ${green("✔")} In sync with cloud`);
|
|
428
|
-
} else {
|
|
429
|
-
console.log(` ${yellow("⚠")} Out of sync — run ${cyan("infernoflow cloud push")} or ${cyan("infernoflow cloud pull")}`);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (pending) {
|
|
434
|
-
console.log(` ${yellow("⚠")} Pending push queued (cloud was unreachable last time)`);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
console.log();
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
async function subcmdDashboard(args, cwd, infernoDir) {
|
|
441
|
-
const config = readCloudConfig(infernoDir);
|
|
442
|
-
const endpoint = getEndpoint(config, args);
|
|
443
|
-
const projectId = config?.projectId;
|
|
444
|
-
const jsonMode = args.includes("--json");
|
|
445
|
-
|
|
446
|
-
if (!projectId) {
|
|
447
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "Run: infernoflow cloud init first" })); }
|
|
448
|
-
else { warn("Not configured. Run: infernoflow cloud init first."); }
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const url = `${endpoint}/p/${projectId}`;
|
|
453
|
-
|
|
454
|
-
if (jsonMode) {
|
|
455
|
-
console.log(JSON.stringify({ ok: true, url }));
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
console.log();
|
|
460
|
-
console.log(` ${bold("🔥 infernoflow cloud dashboard")}`);
|
|
461
|
-
console.log();
|
|
462
|
-
console.log(` ${cyan(url)}`);
|
|
463
|
-
console.log();
|
|
464
|
-
console.log(` ${gray("Share this URL with your whole team.")}`);
|
|
465
|
-
console.log();
|
|
466
|
-
|
|
467
|
-
// Try to open in browser
|
|
468
|
-
try {
|
|
469
|
-
const { execSync } = await import("node:child_process");
|
|
470
|
-
const cmd = process.platform === "win32" ? `start "" "${url}"` :
|
|
471
|
-
process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
472
|
-
execSync(cmd, { stdio: "ignore" });
|
|
473
|
-
} catch {}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
477
|
-
|
|
478
|
-
export async function cloudCommand(rawArgs) {
|
|
479
|
-
const args = rawArgs.slice(1);
|
|
480
|
-
const subcmd = args[0];
|
|
481
|
-
const cwd = process.cwd();
|
|
482
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
483
|
-
|
|
484
|
-
if (!fs.existsSync(infernoDir)) {
|
|
485
|
-
const msg = "inferno/ directory not found. Run: infernoflow init";
|
|
486
|
-
if (args.includes("--json")) { console.log(JSON.stringify({ ok: false, error: msg })); }
|
|
487
|
-
else { warn(msg); }
|
|
488
|
-
process.exit(1);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const subArgs = args.slice(1);
|
|
492
|
-
|
|
493
|
-
switch (subcmd) {
|
|
494
|
-
case "init":
|
|
495
|
-
return subcmdInit(subArgs, cwd, infernoDir);
|
|
496
|
-
case "push":
|
|
497
|
-
return subcmdPush(subArgs, cwd, infernoDir);
|
|
498
|
-
case "pull":
|
|
499
|
-
return subcmdPull(subArgs, cwd, infernoDir);
|
|
500
|
-
case "status":
|
|
501
|
-
return subcmdStatus(subArgs, cwd, infernoDir);
|
|
502
|
-
case "dashboard":
|
|
503
|
-
return subcmdDashboard(subArgs, cwd, infernoDir);
|
|
504
|
-
default: {
|
|
505
|
-
const jsonMode = args.includes("--json");
|
|
506
|
-
const msg = `Unknown cloud sub-command: ${subcmd || "(none)"}. Use: init | push | pull | status | dashboard`;
|
|
507
|
-
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
|
|
508
|
-
else {
|
|
509
|
-
console.log();
|
|
510
|
-
console.log(` ${bold("infernoflow cloud")} — hosted contract sync`);
|
|
511
|
-
console.log();
|
|
512
|
-
console.log(` ${cyan("infernoflow cloud init")} Set up cloud sync for this project`);
|
|
513
|
-
console.log(` ${cyan("infernoflow cloud push")} Upload local contract to cloud`);
|
|
514
|
-
console.log(` ${cyan("infernoflow cloud pull")} Download latest contract from cloud`);
|
|
515
|
-
console.log(` ${cyan("infernoflow cloud status")} Compare local vs cloud`);
|
|
516
|
-
console.log(` ${cyan("infernoflow cloud dashboard")} Open hosted dashboard in browser`);
|
|
517
|
-
console.log();
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
1
|
+
import*as S from"node:fs";import*as j from"node:path";import*as M from"node:https";import*as v from"node:http";import*as E from"node:crypto";import{header as I,ok as q,warn as h,info as O,done as F,bold as m,cyan as y,gray as w,green as U,red as H,yellow as x}from"../ui/output.mjs";const _="https://cloud.infernoflow.dev",A=".cloud.json";function N(n){const r=j.join(n,A);if(!S.existsSync(r))return null;try{return JSON.parse(S.readFileSync(r,"utf8"))}catch{return null}}function B(n,r){const e=j.join(n,A);S.writeFileSync(e,JSON.stringify(r,null,2)+`
|
|
2
|
+
`)}function T(n,r){const e=r.indexOf("--token");return e!==-1?r[e+1]:process.env.INFERNOFLOW_TOKEN||n?.token||null}function k(n,r){const e=r.indexOf("--endpoint");return e!==-1?r[e+1]:process.env.INFERNOFLOW_ENDPOINT||n?.endpoint||_}function R(n,r,e,o){return new Promise((l,a)=>{const t=new URL(r),c=t.protocol==="https:",g=c?M:v,s=e?JSON.stringify(e):null,f={hostname:t.hostname,port:t.port||(c?443:80),path:t.pathname+(t.search||""),method:n,headers:{"Content-Type":"application/json",Accept:"application/json","User-Agent":"infernoflow-cli",...o?{Authorization:`Bearer ${o}`}:{},...s?{"Content-Length":Buffer.byteLength(s)}:{}}},p=g.request(f,i=>{let d="";i.on("data",b=>d+=b),i.on("end",()=>{try{l({status:i.statusCode,body:JSON.parse(d)})}catch{l({status:i.statusCode,body:d})}})});p.on("error",a),s&&p.write(s),p.end()})}function L(n){const r=["contract.json","capabilities.json"];for(const e of r){const o=j.join(n,e);if(S.existsSync(o))try{return JSON.parse(S.readFileSync(o,"utf8"))}catch{}}return null}function J(n){return E.createHash("sha256").update(JSON.stringify(n)).digest("hex").slice(0,12)}async function G(n,r,e){const o=n.includes("--json"),l=k(null,n),a=n.includes("--dry-run"),t=N(e);if(t&&!n.includes("--force")&&!n.includes("-f")){o?console.log(JSON.stringify({ok:!1,error:"Already initialised. Use --force to overwrite.",config:t})):(h("Cloud already configured for this project."),console.log(` Token: ${w(t.token)}`),console.log(` Endpoint: ${w(t.endpoint)}`),console.log(` Project: ${w(t.projectId)}`),console.log(),O("Use --force to generate a new token."));return}const c=E.randomBytes(8).toString("hex"),g=E.randomBytes(24).toString("base64url"),s={projectId:c,token:g,endpoint:l,createdAt:new Date().toISOString()};if(a){o?console.log(JSON.stringify({ok:!0,dryRun:!0,config:s})):(O("Dry run \u2014 would write inferno/.cloud.json:"),console.log(" "+JSON.stringify(s,null,2).split(`
|
|
3
|
+
`).join(`
|
|
4
|
+
`)));return}o||I("Initialising infernoflow cloud");try{const f=await R("POST",`${l}/api/projects`,{projectId:c},null);(f.status===200||f.status===201)&&(o||q("Project registered on cloud"))}catch{o||O("Cloud endpoint unreachable \u2014 saved config locally (will connect on first push)")}B(e,s),o?console.log(JSON.stringify({ok:!0,projectId:c,endpoint:l})):(F("Cloud configured!"),console.log(),console.log(` Project ID: ${y(c)}`),console.log(` Endpoint: ${w(l)}`),console.log(` Token: ${w(g.slice(0,8)+"\u2026")} (stored in inferno/.cloud.json)`),console.log(),console.log(` ${w("Share the dashboard:")} ${y(`${l}/p/${c}`)}`),console.log(),console.log(` ${x("\u26A0")} Add inferno/.cloud.json to .gitignore to protect your token!`),console.log(` ${w("echo 'inferno/.cloud.json' >> .gitignore")}`),console.log())}async function W(n,r,e){const o=n.includes("--json"),l=n.includes("--dry-run"),a=N(e),t=T(a,n),c=k(a,n);if(!t){const i="No token found. Run: infernoflow cloud init";o?console.log(JSON.stringify({ok:!1,error:i})):h(i),process.exit(1)}const g=L(e);if(!g){const i="No contract.json found. Run: infernoflow init";o?console.log(JSON.stringify({ok:!1,error:i})):h(i),process.exit(1)}const s=a?.projectId||"unknown",f=J(g),p=(g.capabilities||[]).length;if(l){o?console.log(JSON.stringify({ok:!0,dryRun:!0,projectId:s,hash:f,capabilities:p})):O(`Dry run \u2014 would push ${m(String(p))} capabilities (hash: ${f}) to ${c}`);return}o||I("Pushing contract to cloud");try{const i=await R("PUT",`${c}/api/projects/${s}/contract`,{contract:g,hash:f,pushedAt:new Date().toISOString()},t);if(i.status===200||i.status===201||i.status===204)o?console.log(JSON.stringify({ok:!0,projectId:s,hash:f,capabilities:p})):(F(`Pushed ${m(String(p))} capabilities`),console.log(` ${w("Dashboard:")} ${y(`${c}/p/${s}`)}`),console.log());else{const d=`Cloud returned ${i.status}`;o?console.log(JSON.stringify({ok:!1,error:d,status:i.status})):h(d),process.exit(1)}}catch(i){const d=j.join(e,".cloud-pending.json");S.writeFileSync(d,JSON.stringify({hash:f,pendingAt:new Date().toISOString()})),o?console.log(JSON.stringify({ok:!1,error:i.message,pending:!0})):(h("Cloud unreachable \u2014 push queued locally."),O("Changes will sync automatically on next successful connection."))}}async function z(n,r,e){const o=n.includes("--json"),l=n.includes("--dry-run"),a=N(e),t=T(a,n),c=k(a,n);if(!t){const s="No token found. Run: infernoflow cloud init";o?console.log(JSON.stringify({ok:!1,error:s})):h(s),process.exit(1)}const g=a?.projectId||"unknown";o||I("Pulling contract from cloud");try{const s=await R("GET",`${c}/api/projects/${g}/contract`,null,t);if(s.status!==200){const u=`Cloud returned ${s.status}`;o?console.log(JSON.stringify({ok:!1,error:u})):h(u),process.exit(1)}const f=s.body?.contract,p=L(e);if(!f){const u="No contract found on cloud. Push first.";o?console.log(JSON.stringify({ok:!1,error:u})):h(u);return}const i=(p?.capabilities||[]).map(u=>typeof u=="string"?u:u.id),d=(f.capabilities||[]).map(u=>typeof u=="string"?u:u.id),b=new Set(i),C=new Set(d),$=i.filter(u=>!C.has(u)),P=d.filter(u=>!b.has(u));if($.length>0&&P.length>0&&(o?console.log(JSON.stringify({ok:!1,conflict:!0,onlyLocal:$,onlyRemote:P})):(h("Diverged contracts detected:"),$.forEach(u=>console.log(` ${H("-")} local-only: ${u}`)),P.forEach(u=>console.log(` ${U("+")} remote-only: ${u}`)),console.log(),h("Merge manually or use --force to overwrite local with remote.")),!n.includes("--force")&&!n.includes("-f")))return;if(l){o?console.log(JSON.stringify({ok:!0,dryRun:!0,capabilities:d.length,hash:J(f)})):O(`Dry run \u2014 would write ${m(String(d.length))} capabilities from cloud`);return}const D=j.join(e,"contract.json");S.writeFileSync(D,JSON.stringify(f,null,2)+`
|
|
5
|
+
`),o?console.log(JSON.stringify({ok:!0,capabilities:d.length,hash:J(f)})):(F(`Pulled ${m(String(d.length))} capabilities from cloud`),$.length&&h(`${$.length} local-only capabilities were overwritten.`),console.log())}catch(s){o?console.log(JSON.stringify({ok:!1,error:s.message})):h(`Cloud unreachable: ${s.message}`),process.exit(1)}}async function K(n,r,e){const o=n.includes("--json"),l=N(e),a=T(l,n),t=k(l,n);if(!l){o?console.log(JSON.stringify({ok:!1,error:"Not initialised. Run: infernoflow cloud init"})):h("Cloud not configured. Run: infernoflow cloud init");return}const c=l.projectId,g=L(e),s=g?J(g):null,f=(g?.capabilities||[]).length;o||I("Cloud status");let p=null,i=0,d=!1;try{const $=await R("GET",`${t}/api/projects/${c}/contract`,null,a);$.status===200&&$.body?.contract&&(d=!0,p=J($.body.contract),i=($.body.contract?.capabilities||[]).length)}catch{}const b=s===p,C=S.existsSync(j.join(e,".cloud-pending.json"));if(o){console.log(JSON.stringify({ok:!0,projectId:c,endpoint:t,reachable:d,inSync:b,pending:C,local:{hash:s,capabilities:f},remote:d?{hash:p,capabilities:i}:null}));return}console.log(` Project: ${y(c)}`),console.log(` Endpoint: ${w(t)}`),console.log(` Dashboard: ${y(`${t}/p/${c}`)}`),console.log(),console.log(` Local: ${m(String(f))} capabilities ${w("(hash: "+(s||"none")+")")}`),d?(console.log(` Cloud: ${m(String(i))} capabilities ${w("(hash: "+(p||"none")+")")}`),console.log(),console.log(b?` ${U("\u2714")} In sync with cloud`:` ${x("\u26A0")} Out of sync \u2014 run ${y("infernoflow cloud push")} or ${y("infernoflow cloud pull")}`)):console.log(` Cloud: ${x("unreachable")}`),C&&console.log(` ${x("\u26A0")} Pending push queued (cloud was unreachable last time)`),console.log()}async function Q(n,r,e){const o=N(e),l=k(o,n),a=o?.projectId,t=n.includes("--json");if(!a){t?console.log(JSON.stringify({ok:!1,error:"Run: infernoflow cloud init first"})):h("Not configured. Run: infernoflow cloud init first.");return}const c=`${l}/p/${a}`;if(t){console.log(JSON.stringify({ok:!0,url:c}));return}console.log(),console.log(` ${m("\u{1F525} infernoflow cloud dashboard")}`),console.log(),console.log(` ${y(c)}`),console.log(),console.log(` ${w("Share this URL with your whole team.")}`),console.log();try{const{execSync:g}=await import("node:child_process"),s=process.platform==="win32"?`start "" "${c}"`:process.platform==="darwin"?`open "${c}"`:`xdg-open "${c}"`;g(s,{stdio:"ignore"})}catch{}}async function X(n){const r=n.slice(1),e=r[0],o=process.cwd(),l=j.join(o,"inferno");if(!S.existsSync(l)){const t="inferno/ directory not found. Run: infernoflow init";r.includes("--json")?console.log(JSON.stringify({ok:!1,error:t})):h(t),process.exit(1)}const a=r.slice(1);switch(e){case"init":return G(a,o,l);case"push":return W(a,o,l);case"pull":return z(a,o,l);case"status":return K(a,o,l);case"dashboard":return Q(a,o,l);default:{const t=r.includes("--json"),c=`Unknown cloud sub-command: ${e||"(none)"}. Use: init | push | pull | status | dashboard`;t?console.log(JSON.stringify({ok:!1,error:c})):(console.log(),console.log(` ${m("infernoflow cloud")} \u2014 hosted contract sync`),console.log(),console.log(` ${y("infernoflow cloud init")} Set up cloud sync for this project`),console.log(` ${y("infernoflow cloud push")} Upload local contract to cloud`),console.log(` ${y("infernoflow cloud pull")} Download latest contract from cloud`),console.log(` ${y("infernoflow cloud status")} Compare local vs cloud`),console.log(` ${y("infernoflow cloud dashboard")} Open hosted dashboard in browser`),console.log())}}}export{X as cloudCommand};
|