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.
Files changed (78) hide show
  1. package/dist/bin/infernoflow.mjs +84 -255
  2. package/dist/lib/adopters/angular.mjs +1 -128
  3. package/dist/lib/adopters/css.mjs +1 -111
  4. package/dist/lib/adopters/react.mjs +1 -104
  5. package/dist/lib/ai/ideDetection.mjs +1 -31
  6. package/dist/lib/ai/localProvider.mjs +1 -88
  7. package/dist/lib/ai/providerRouter.mjs +2 -295
  8. package/dist/lib/commands/adopt.mjs +20 -869
  9. package/dist/lib/commands/adoptWizard.mjs +9 -320
  10. package/dist/lib/commands/agent.mjs +5 -191
  11. package/dist/lib/commands/ai.mjs +2 -407
  12. package/dist/lib/commands/audit.mjs +13 -300
  13. package/dist/lib/commands/changelog.mjs +26 -594
  14. package/dist/lib/commands/check.mjs +3 -184
  15. package/dist/lib/commands/ci.mjs +3 -208
  16. package/dist/lib/commands/claudeMd.mjs +25 -130
  17. package/dist/lib/commands/cloud.mjs +5 -521
  18. package/dist/lib/commands/context.mjs +31 -287
  19. package/dist/lib/commands/coverage.mjs +2 -282
  20. package/dist/lib/commands/dashboard.mjs +123 -635
  21. package/dist/lib/commands/demo.mjs +8 -465
  22. package/dist/lib/commands/diff.mjs +5 -274
  23. package/dist/lib/commands/docGate.mjs +2 -81
  24. package/dist/lib/commands/doctor.mjs +3 -321
  25. package/dist/lib/commands/explain.mjs +8 -438
  26. package/dist/lib/commands/export.mjs +10 -239
  27. package/dist/lib/commands/generateSkills.mjs +38 -163
  28. package/dist/lib/commands/graph.mjs +203 -320
  29. package/dist/lib/commands/health.mjs +2 -309
  30. package/dist/lib/commands/impact.mjs +2 -325
  31. package/dist/lib/commands/implement.mjs +7 -103
  32. package/dist/lib/commands/init.mjs +23 -475
  33. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  34. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  35. package/dist/lib/commands/link.mjs +2 -342
  36. package/dist/lib/commands/monorepo.mjs +4 -428
  37. package/dist/lib/commands/notify.mjs +4 -258
  38. package/dist/lib/commands/onboard.mjs +4 -296
  39. package/dist/lib/commands/prComment.mjs +2 -361
  40. package/dist/lib/commands/prImpact.mjs +2 -157
  41. package/dist/lib/commands/publish.mjs +15 -316
  42. package/dist/lib/commands/report.mjs +28 -272
  43. package/dist/lib/commands/review.mjs +9 -223
  44. package/dist/lib/commands/run.mjs +8 -336
  45. package/dist/lib/commands/scaffold.mjs +54 -419
  46. package/dist/lib/commands/scan.mjs +5 -558
  47. package/dist/lib/commands/scout.mjs +2 -291
  48. package/dist/lib/commands/setup.mjs +5 -310
  49. package/dist/lib/commands/share.mjs +13 -196
  50. package/dist/lib/commands/snapshot.mjs +3 -383
  51. package/dist/lib/commands/stability.mjs +2 -293
  52. package/dist/lib/commands/status.mjs +4 -172
  53. package/dist/lib/commands/suggest.mjs +21 -563
  54. package/dist/lib/commands/syncAuto.mjs +1 -96
  55. package/dist/lib/commands/synthesize.mjs +10 -228
  56. package/dist/lib/commands/teamSync.mjs +2 -388
  57. package/dist/lib/commands/test.mjs +6 -363
  58. package/dist/lib/commands/version.mjs +2 -282
  59. package/dist/lib/commands/vibe.mjs +7 -357
  60. package/dist/lib/commands/watch.mjs +4 -203
  61. package/dist/lib/commands/why.mjs +4 -358
  62. package/dist/lib/cursorHooksInstall.mjs +1 -60
  63. package/dist/lib/draftToolingInstall.mjs +7 -68
  64. package/dist/lib/git/detect-drift.mjs +4 -208
  65. package/dist/lib/learning/adapt.mjs +6 -101
  66. package/dist/lib/learning/observe.mjs +1 -119
  67. package/dist/lib/learning/patternDetector.mjs +1 -298
  68. package/dist/lib/learning/profile.mjs +2 -279
  69. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  70. package/dist/lib/templates/index.mjs +1 -131
  71. package/dist/lib/ui/errors.mjs +1 -142
  72. package/dist/lib/ui/output.mjs +6 -72
  73. package/dist/lib/ui/prompts.mjs +6 -147
  74. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  75. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  76. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  77. package/dist/templates/github-app/app-manifest.json +20 -0
  78. package/package.json +1 -1
@@ -1,521 +1,5 @@
1
- /**
2
- * infernoflow cloud
3
- *
4
- * Sync capability contracts with the infernoflow cloud service.
5
- * A hosted alternative to `team-sync` (which uses a shared git branch).
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};