infernoflow 0.32.8 → 0.33.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.
Files changed (81) 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 +34 -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 -321
  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/log.mjs +16 -0
  37. package/dist/lib/commands/monorepo.mjs +4 -428
  38. package/dist/lib/commands/notify.mjs +4 -258
  39. package/dist/lib/commands/onboard.mjs +4 -296
  40. package/dist/lib/commands/prComment.mjs +2 -361
  41. package/dist/lib/commands/prImpact.mjs +2 -157
  42. package/dist/lib/commands/publish.mjs +15 -316
  43. package/dist/lib/commands/report.mjs +28 -272
  44. package/dist/lib/commands/review.mjs +9 -223
  45. package/dist/lib/commands/run.mjs +8 -336
  46. package/dist/lib/commands/scaffold.mjs +54 -419
  47. package/dist/lib/commands/scan.mjs +5 -558
  48. package/dist/lib/commands/scout.mjs +2 -291
  49. package/dist/lib/commands/setup.mjs +5 -310
  50. package/dist/lib/commands/share.mjs +13 -196
  51. package/dist/lib/commands/snapshot.mjs +3 -383
  52. package/dist/lib/commands/stability.mjs +2 -293
  53. package/dist/lib/commands/status.mjs +4 -172
  54. package/dist/lib/commands/suggest.mjs +21 -563
  55. package/dist/lib/commands/syncAuto.mjs +1 -96
  56. package/dist/lib/commands/synthesize.mjs +10 -228
  57. package/dist/lib/commands/teamSync.mjs +2 -388
  58. package/dist/lib/commands/test.mjs +6 -363
  59. package/dist/lib/commands/theme.mjs +18 -0
  60. package/dist/lib/commands/version.mjs +2 -282
  61. package/dist/lib/commands/vibe.mjs +7 -357
  62. package/dist/lib/commands/watch.mjs +4 -203
  63. package/dist/lib/commands/why.mjs +4 -358
  64. package/dist/lib/cursorHooksInstall.mjs +1 -60
  65. package/dist/lib/draftToolingInstall.mjs +7 -68
  66. package/dist/lib/git/detect-drift.mjs +4 -208
  67. package/dist/lib/learning/adapt.mjs +6 -101
  68. package/dist/lib/learning/observe.mjs +1 -119
  69. package/dist/lib/learning/patternDetector.mjs +1 -298
  70. package/dist/lib/learning/profile.mjs +2 -279
  71. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  72. package/dist/lib/templates/index.mjs +1 -131
  73. package/dist/lib/theme/scanner.mjs +4 -0
  74. package/dist/lib/ui/errors.mjs +1 -142
  75. package/dist/lib/ui/output.mjs +6 -72
  76. package/dist/lib/ui/prompts.mjs +6 -147
  77. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  78. package/dist/templates/cursor/inferno-mcp-server.mjs +29 -0
  79. package/dist/templates/github-app/GITHUB_APP.md +67 -0
  80. package/dist/templates/github-app/app-manifest.json +20 -0
  81. 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};