infernoflow 0.37.0 → 0.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -517
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,773 +1,10 @@
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 push --memory Also push session memory (sessions.jsonl) Pro tier value prop
11
- * cloud pull Download latest contract from cloud
12
- * cloud pull --memory Also pull session memory — restores inferno/sessions.jsonl
13
- * cloud memory push Push session memory only
14
- * cloud memory pull Pull session memory only
15
- * cloud memory status Compare local vs remote memory entry count
16
- * cloud status Show local vs cloud diff
17
- * cloud dashboard Print hosted dashboard URL
18
- *
19
- * Flags:
20
- * --token <tok> Override token from env INFERNOFLOW_TOKEN
21
- * --endpoint <url> Override default endpoint
22
- * --dry-run Print what would happen without sending
23
- * --json Machine-readable output
24
- * --memory Include session memory (sessions.jsonl) in push/pull
25
- *
26
- * Usage:
27
- * infernoflow cloud init
28
- * infernoflow cloud push
29
- * infernoflow cloud push --memory
30
- * infernoflow cloud pull --memory
31
- * infernoflow cloud memory status --json
32
- */
33
-
34
- import * as fs from "node:fs";
35
- import * as path from "node:path";
36
- import * as https from "node:https";
37
- import * as http from "node:http";
38
- import * as crypto from "node:crypto";
39
- import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
40
-
41
- // ── Config ────────────────────────────────────────────────────────────────────
42
-
43
- const DEFAULT_ENDPOINT = "https://cloud.infernoflow.dev";
44
- const CLOUD_CONFIG_FILE = ".cloud.json";
45
-
46
- function readCloudConfig(infernoDir) {
47
- const p = path.join(infernoDir, CLOUD_CONFIG_FILE);
48
- if (!fs.existsSync(p)) return null;
49
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
50
- }
51
-
52
- function writeCloudConfig(infernoDir, config) {
53
- const p = path.join(infernoDir, CLOUD_CONFIG_FILE);
54
- fs.writeFileSync(p, JSON.stringify(config, null, 2) + "\n");
55
- }
56
-
57
- function getToken(config, args) {
58
- const idx = args.indexOf("--token");
59
- if (idx !== -1) return args[idx + 1];
60
- return process.env.INFERNOFLOW_TOKEN || config?.token || null;
61
- }
62
-
63
- function getEndpoint(config, args) {
64
- const idx = args.indexOf("--endpoint");
65
- if (idx !== -1) return args[idx + 1];
66
- return process.env.INFERNOFLOW_ENDPOINT || config?.endpoint || DEFAULT_ENDPOINT;
67
- }
68
-
69
- // ── HTTP helpers ──────────────────────────────────────────────────────────────
70
-
71
- function httpsRequest(method, url, body, token) {
72
- return new Promise((resolve, reject) => {
73
- const parsed = new URL(url);
74
- const isHttps = parsed.protocol === "https:";
75
- const lib = isHttps ? https : http;
76
- const payload = body ? JSON.stringify(body) : null;
77
-
78
- const options = {
79
- hostname: parsed.hostname,
80
- port: parsed.port || (isHttps ? 443 : 80),
81
- path: parsed.pathname + (parsed.search || ""),
82
- method,
83
- headers: {
84
- "Content-Type": "application/json",
85
- "Accept": "application/json",
86
- "User-Agent": "infernoflow-cli",
87
- ...(token ? { "Authorization": `Bearer ${token}` } : {}),
88
- ...(payload ? { "Content-Length": Buffer.byteLength(payload) } : {}),
89
- },
90
- };
91
-
92
- const req = lib.request(options, (res) => {
93
- let data = "";
94
- res.on("data", (chunk) => (data += chunk));
95
- res.on("end", () => {
96
- try {
97
- resolve({ status: res.statusCode, body: JSON.parse(data) });
98
- } catch {
99
- resolve({ status: res.statusCode, body: data });
100
- }
101
- });
102
- });
103
-
104
- req.on("error", reject);
105
- if (payload) req.write(payload);
106
- req.end();
107
- });
108
- }
109
-
110
- // ── Contract helpers ──────────────────────────────────────────────────────────
111
-
112
- function readContract(infernoDir) {
113
- const candidates = ["contract.json", "capabilities.json"];
114
- for (const f of candidates) {
115
- const p = path.join(infernoDir, f);
116
- if (fs.existsSync(p)) {
117
- try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
118
- }
119
- }
120
- return null;
121
- }
122
-
123
- function contractHash(contract) {
124
- return crypto.createHash("sha256").update(JSON.stringify(contract)).digest("hex").slice(0, 12);
125
- }
126
-
127
- // ── Session memory helpers ────────────────────────────────────────────────────
128
-
129
- const SESSIONS_FILE = "sessions.jsonl";
130
-
131
- function readMemory(infernoDir) {
132
- const p = path.join(infernoDir, SESSIONS_FILE);
133
- if (!fs.existsSync(p)) return [];
134
- return fs.readFileSync(p, "utf8")
135
- .split("\n").filter(Boolean)
136
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
137
- .filter(Boolean);
138
- }
139
-
140
- function writeMemory(infernoDir, entries) {
141
- const p = path.join(infernoDir, SESSIONS_FILE);
142
- fs.writeFileSync(p, entries.map(e => JSON.stringify(e)).join("\n") + "\n", "utf8");
143
- }
144
-
145
- function memoryHash(entries) {
146
- return crypto.createHash("sha256")
147
- .update(JSON.stringify(entries.map(e => e.ts + e.summary)))
148
- .digest("hex").slice(0, 12);
149
- }
150
-
151
- /**
152
- * Merge remote memory with local — union by (ts, summary) deduplication.
153
- * Keeps all local entries + any remote entries not already present.
154
- */
155
- function mergeMemory(local, remote) {
156
- const localKeys = new Set(local.map(e => `${e.ts}|${e.summary}`));
157
- const merged = [...local];
158
- for (const e of remote) {
159
- if (!localKeys.has(`${e.ts}|${e.summary}`)) {
160
- merged.push(e);
161
- }
162
- }
163
- return merged.sort((a, b) => a.ts.localeCompare(b.ts));
164
- }
165
-
166
- // ── Memory push/pull ──────────────────────────────────────────────────────────
167
-
168
- async function pushMemory(args, infernoDir, config, quietly = false) {
169
- const jsonMode = args.includes("--json");
170
- const dryRun = args.includes("--dry-run");
171
- const token = getToken(config, args);
172
- const endpoint = getEndpoint(config, args);
173
- const projectId = config?.projectId;
174
-
175
- if (!token || !projectId) {
176
- const msg = "No token/project found. Run: infernoflow cloud init";
177
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
178
- else if (!quietly) warn(msg);
179
- return { ok: false };
180
- }
181
-
182
- const entries = readMemory(infernoDir);
183
- if (!entries.length) {
184
- if (!quietly && !jsonMode) info("No session memory to push (inferno/sessions.jsonl is empty).");
185
- return { ok: true, entries: 0 };
186
- }
187
-
188
- const hash = memoryHash(entries);
189
-
190
- if (dryRun) {
191
- if (jsonMode) console.log(JSON.stringify({ ok: true, dryRun: true, entries: entries.length, hash }));
192
- else if (!quietly) info(`Dry run — would push ${bold(String(entries.length))} memory entries (hash: ${hash})`);
193
- return { ok: true, dryRun: true };
194
- }
195
-
196
- try {
197
- const resp = await httpsRequest(
198
- "PUT",
199
- `${endpoint}/api/projects/${projectId}/memory`,
200
- { entries, hash, pushedAt: new Date().toISOString() },
201
- token
202
- );
203
- const ok_flag = resp.status === 200 || resp.status === 201 || resp.status === 204;
204
- if (jsonMode) console.log(JSON.stringify({ ok: ok_flag, entries: entries.length, hash }));
205
- else if (!quietly) {
206
- if (ok_flag) ok(`Pushed ${bold(String(entries.length))} memory entries`);
207
- else warn(`Cloud returned ${resp.status}`);
208
- }
209
- return { ok: ok_flag, entries: entries.length };
210
- } catch (err) {
211
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: err.message }));
212
- else if (!quietly) warn(`Memory push failed: ${err.message}`);
213
- return { ok: false };
214
- }
215
- }
216
-
217
- async function pullMemory(args, infernoDir, config, quietly = false) {
218
- const jsonMode = args.includes("--json");
219
- const dryRun = args.includes("--dry-run");
220
- const token = getToken(config, args);
221
- const endpoint = getEndpoint(config, args);
222
- const projectId = config?.projectId;
223
- const forceOverwrite = args.includes("--force") || args.includes("-f");
224
-
225
- if (!token || !projectId) {
226
- const msg = "No token/project found. Run: infernoflow cloud init";
227
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
228
- else if (!quietly) warn(msg);
229
- return { ok: false };
230
- }
231
-
232
- try {
233
- const resp = await httpsRequest(
234
- "GET",
235
- `${endpoint}/api/projects/${projectId}/memory`,
236
- null,
237
- token
238
- );
239
-
240
- if (resp.status !== 200) {
241
- const errMsg = `Cloud returned ${resp.status}`;
242
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: errMsg }));
243
- else if (!quietly) warn(errMsg);
244
- return { ok: false };
245
- }
246
-
247
- const remote = resp.body?.entries;
248
- if (!remote || !remote.length) {
249
- if (!quietly && !jsonMode) info("No session memory in cloud yet. Push first.");
250
- return { ok: true, entries: 0 };
251
- }
252
-
253
- const local = readMemory(infernoDir);
254
- const merged = forceOverwrite ? remote : mergeMemory(local, remote);
255
- const newCount = merged.length - local.length;
256
-
257
- if (dryRun) {
258
- if (jsonMode) console.log(JSON.stringify({ ok: true, dryRun: true, remote: remote.length, local: local.length, merged: merged.length }));
259
- else if (!quietly) info(`Dry run — would merge ${bold(String(remote.length))} remote + ${bold(String(local.length))} local = ${bold(String(merged.length))} entries`);
260
- return { ok: true, dryRun: true };
261
- }
262
-
263
- writeMemory(infernoDir, merged);
264
-
265
- if (jsonMode) console.log(JSON.stringify({ ok: true, remote: remote.length, local: local.length, merged: merged.length, newEntries: newCount }));
266
- else if (!quietly) ok(`Merged ${bold(String(remote.length))} remote entries → ${bold(String(merged.length))} total (${newCount} new)`);
267
- return { ok: true, entries: merged.length };
268
- } catch (err) {
269
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: err.message }));
270
- else if (!quietly) warn(`Memory pull failed: ${err.message}`);
271
- return { ok: false };
272
- }
273
- }
274
-
275
- async function subcmdMemory(args, cwd, infernoDir) {
276
- const jsonMode = args.includes("--json");
277
- const config = readCloudConfig(infernoDir);
278
- const token = getToken(config, args);
279
- const endpoint = getEndpoint(config, args);
280
-
281
- const sub2 = args[0];
282
- const sub2Args = args.slice(1);
283
-
284
- if (sub2 === "push") {
285
- if (!jsonMode) header("Pushing session memory to cloud");
286
- return pushMemory(sub2Args, infernoDir, config);
287
- }
288
-
289
- if (sub2 === "pull") {
290
- if (!jsonMode) header("Pulling session memory from cloud");
291
- return pullMemory(sub2Args, infernoDir, config);
292
- }
293
-
294
- if (sub2 === "status" || !sub2) {
295
- const local = readMemory(infernoDir);
296
- const projectId = config?.projectId;
297
-
298
- if (!config || !token) {
299
- if (jsonMode) console.log(JSON.stringify({ ok: false, error: "Not initialised. Run: infernoflow cloud init" }));
300
- else warn("Cloud not configured. Run: infernoflow cloud init");
301
- return;
302
- }
303
-
304
- let remoteCount = null;
305
- let remoteHash = null;
306
- let reachable = false;
307
-
308
- try {
309
- const resp = await httpsRequest("GET", `${endpoint}/api/projects/${projectId}/memory`, null, token);
310
- if (resp.status === 200 && resp.body?.entries) {
311
- reachable = true;
312
- remoteCount = resp.body.entries.length;
313
- remoteHash = memoryHash(resp.body.entries);
314
- }
315
- } catch {}
316
-
317
- const localHash = local.length ? memoryHash(local) : null;
318
-
319
- if (jsonMode) {
320
- console.log(JSON.stringify({
321
- ok: true,
322
- local: { entries: local.length, hash: localHash },
323
- remote: reachable ? { entries: remoteCount, hash: remoteHash } : null,
324
- reachable,
325
- inSync: localHash === remoteHash,
326
- }));
327
- return;
328
- }
329
-
330
- console.log();
331
- console.log(` ${bold("infernoflow cloud memory status")}`);
332
- console.log();
333
- console.log(` Local: ${bold(String(local.length))} entries ${gray("(hash: " + (localHash || "none") + ")")}`);
334
- if (!reachable) {
335
- console.log(` Cloud: ${yellow("unreachable")}`);
336
- } else {
337
- console.log(` Cloud: ${bold(String(remoteCount))} entries ${gray("(hash: " + (remoteHash || "none") + ")")}`);
338
- if (localHash === remoteHash) console.log(`\n ${green("✔")} Memory in sync`);
339
- else console.log(`\n ${yellow("⚠")} Out of sync — run ${cyan("infernoflow cloud memory push")} or ${cyan("infernoflow cloud memory pull")}`);
340
- }
341
- console.log();
342
- return;
343
- }
344
-
345
- console.log();
346
- console.log(` ${bold("infernoflow cloud memory")} — session memory sync`);
347
- console.log();
348
- console.log(` ${cyan("infernoflow cloud memory push")} Upload sessions.jsonl to cloud`);
349
- console.log(` ${cyan("infernoflow cloud memory pull")} Download + merge remote memory`);
350
- console.log(` ${cyan("infernoflow cloud memory status")} Compare local vs remote`);
351
- console.log();
352
- }
353
-
354
- // ── Sub-commands ──────────────────────────────────────────────────────────────
355
-
356
- async function subcmdInit(args, cwd, infernoDir) {
357
- const jsonMode = args.includes("--json");
358
- const endpoint = getEndpoint(null, args);
359
- const dryRun = args.includes("--dry-run");
360
-
361
- // Check for existing config
362
- const existing = readCloudConfig(infernoDir);
363
- if (existing && !args.includes("--force") && !args.includes("-f")) {
364
- if (jsonMode) {
365
- console.log(JSON.stringify({ ok: false, error: "Already initialised. Use --force to overwrite.", config: existing }));
366
- } else {
367
- warn("Cloud already configured for this project.");
368
- console.log(` Token: ${gray(existing.token)}`);
369
- console.log(` Endpoint: ${gray(existing.endpoint)}`);
370
- console.log(` Project: ${gray(existing.projectId)}`);
371
- console.log();
372
- info("Use --force to generate a new token.");
373
- }
374
- return;
375
- }
376
-
377
- // Generate a project ID and token
378
- const projectId = crypto.randomBytes(8).toString("hex");
379
- const token = crypto.randomBytes(24).toString("base64url");
380
-
381
- const config = {
382
- projectId,
383
- token,
384
- endpoint,
385
- createdAt: new Date().toISOString(),
386
- };
387
-
388
- if (dryRun) {
389
- if (jsonMode) {
390
- console.log(JSON.stringify({ ok: true, dryRun: true, config }));
391
- } else {
392
- info("Dry run — would write inferno/.cloud.json:");
393
- console.log(" " + JSON.stringify(config, null, 2).split("\n").join("\n "));
394
- }
395
- return;
396
- }
397
-
398
- if (!jsonMode) header("Initialising infernoflow cloud");
399
-
400
- // Register project with cloud endpoint (best-effort)
401
- try {
402
- const resp = await httpsRequest("POST", `${endpoint}/api/projects`, { projectId }, null);
403
- if (resp.status === 200 || resp.status === 201) {
404
- if (!jsonMode) ok("Project registered on cloud");
405
- }
406
- } catch {
407
- if (!jsonMode) info("Cloud endpoint unreachable — saved config locally (will connect on first push)");
408
- }
409
-
410
- writeCloudConfig(infernoDir, config);
411
-
412
- if (jsonMode) {
413
- console.log(JSON.stringify({ ok: true, projectId, endpoint }));
414
- } else {
415
- done("Cloud configured!");
416
- console.log();
417
- console.log(` Project ID: ${cyan(projectId)}`);
418
- console.log(` Endpoint: ${gray(endpoint)}`);
419
- console.log(` Token: ${gray(token.slice(0, 8) + "…")} (stored in inferno/.cloud.json)`);
420
- console.log();
421
- console.log(` ${gray("Share the dashboard:")} ${cyan(`${endpoint}/p/${projectId}`)}`);
422
- console.log();
423
- console.log(` ${yellow("⚠")} Add inferno/.cloud.json to .gitignore to protect your token!`);
424
- console.log(` ${gray("echo 'inferno/.cloud.json' >> .gitignore")}`);
425
- console.log();
426
- }
427
- }
428
-
429
- async function subcmdPush(args, cwd, infernoDir) {
430
- const jsonMode = args.includes("--json");
431
- const dryRun = args.includes("--dry-run");
432
- const config = readCloudConfig(infernoDir);
433
- const token = getToken(config, args);
434
- const endpoint = getEndpoint(config, args);
435
-
436
- if (!token) {
437
- const msg = "No token found. Run: infernoflow cloud init";
438
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
439
- process.exit(1);
440
- }
441
-
442
- const contract = readContract(infernoDir);
443
- if (!contract) {
444
- const msg = "No contract.json found. Run: infernoflow init";
445
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
446
- process.exit(1);
447
- }
448
-
449
- const projectId = config?.projectId || "unknown";
450
- const hash = contractHash(contract);
451
- const caps = (contract.capabilities || []).length;
452
-
453
- if (dryRun) {
454
- if (jsonMode) {
455
- console.log(JSON.stringify({ ok: true, dryRun: true, projectId, hash, capabilities: caps }));
456
- } else {
457
- info(`Dry run — would push ${bold(String(caps))} capabilities (hash: ${hash}) to ${endpoint}`);
458
- }
459
- return;
460
- }
461
-
462
- if (!jsonMode) header("Pushing contract to cloud");
463
-
464
- try {
465
- const resp = await httpsRequest(
466
- "PUT",
467
- `${endpoint}/api/projects/${projectId}/contract`,
468
- { contract, hash, pushedAt: new Date().toISOString() },
469
- token
470
- );
471
-
472
- if (resp.status === 200 || resp.status === 201 || resp.status === 204) {
473
- if (jsonMode) {
474
- console.log(JSON.stringify({ ok: true, projectId, hash, capabilities: caps }));
475
- } else {
476
- done(`Pushed ${bold(String(caps))} capabilities`);
477
- console.log(` ${gray("Dashboard:")} ${cyan(`${endpoint}/p/${projectId}`)}`);
478
- console.log();
479
- }
480
-
481
- // Also push session memory if --memory flag set
482
- if (args.includes("--memory")) {
483
- if (!jsonMode) info("Pushing session memory...");
484
- await pushMemory(args, infernoDir, config, jsonMode);
485
- if (!jsonMode) ok("Session memory pushed");
486
- }
487
- } else {
488
- const errMsg = `Cloud returned ${resp.status}`;
489
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: errMsg, status: resp.status })); }
490
- else { warn(errMsg); }
491
- process.exit(1);
492
- }
493
- } catch (err) {
494
- // Cloud unreachable — save a pending push marker
495
- const pendingPath = path.join(infernoDir, ".cloud-pending.json");
496
- fs.writeFileSync(pendingPath, JSON.stringify({ hash, pendingAt: new Date().toISOString() }));
497
-
498
- if (jsonMode) {
499
- console.log(JSON.stringify({ ok: false, error: err.message, pending: true }));
500
- } else {
501
- warn("Cloud unreachable — push queued locally.");
502
- info("Changes will sync automatically on next successful connection.");
503
- }
504
- }
505
- }
506
-
507
- async function subcmdPull(args, cwd, infernoDir) {
508
- const jsonMode = args.includes("--json");
509
- const dryRun = args.includes("--dry-run");
510
- const config = readCloudConfig(infernoDir);
511
- const token = getToken(config, args);
512
- const endpoint = getEndpoint(config, args);
513
-
514
- if (!token) {
515
- const msg = "No token found. Run: infernoflow cloud init";
516
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); } else { warn(msg); }
517
- process.exit(1);
518
- }
519
-
520
- const projectId = config?.projectId || "unknown";
521
-
522
- if (!jsonMode) header("Pulling contract from cloud");
523
-
524
- try {
525
- const resp = await httpsRequest(
526
- "GET",
527
- `${endpoint}/api/projects/${projectId}/contract`,
528
- null,
529
- token
530
- );
531
-
532
- if (resp.status !== 200) {
533
- const errMsg = `Cloud returned ${resp.status}`;
534
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: errMsg })); }
535
- else { warn(errMsg); }
536
- process.exit(1);
537
- }
538
-
539
- const remote = resp.body?.contract;
540
- const localRaw = readContract(infernoDir);
541
-
542
- if (!remote) {
543
- const msg = "No contract found on cloud. Push first.";
544
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
545
- else { warn(msg); }
546
- return;
547
- }
548
-
549
- // Detect conflicts (same capability changed on both sides)
550
- const localCaps = (localRaw?.capabilities || []).map(c => typeof c === "string" ? c : c.id);
551
- const remoteCaps = (remote.capabilities || []).map(c => typeof c === "string" ? c : c.id);
552
- const localSet = new Set(localCaps);
553
- const remoteSet = new Set(remoteCaps);
554
- const onlyLocal = localCaps.filter(id => !remoteSet.has(id));
555
- const onlyRemote = remoteCaps.filter(id => !localSet.has(id));
556
-
557
- if (onlyLocal.length > 0 && onlyRemote.length > 0) {
558
- if (!jsonMode) {
559
- warn("Diverged contracts detected:");
560
- onlyLocal.forEach(id => console.log(` ${red("-")} local-only: ${id}`));
561
- onlyRemote.forEach(id => console.log(` ${green("+")} remote-only: ${id}`));
562
- console.log();
563
- warn("Merge manually or use --force to overwrite local with remote.");
564
- } else {
565
- console.log(JSON.stringify({
566
- ok: false,
567
- conflict: true,
568
- onlyLocal,
569
- onlyRemote,
570
- }));
571
- }
572
- if (!args.includes("--force") && !args.includes("-f")) return;
573
- }
574
-
575
- if (dryRun) {
576
- if (jsonMode) {
577
- console.log(JSON.stringify({ ok: true, dryRun: true, capabilities: remoteCaps.length, hash: contractHash(remote) }));
578
- } else {
579
- info(`Dry run — would write ${bold(String(remoteCaps.length))} capabilities from cloud`);
580
- }
581
- return;
582
- }
583
-
584
- // Write pulled contract
585
- const contractPath = path.join(infernoDir, "contract.json");
586
- fs.writeFileSync(contractPath, JSON.stringify(remote, null, 2) + "\n");
587
-
588
- if (jsonMode) {
589
- console.log(JSON.stringify({ ok: true, capabilities: remoteCaps.length, hash: contractHash(remote) }));
590
- } else {
591
- done(`Pulled ${bold(String(remoteCaps.length))} capabilities from cloud`);
592
- if (onlyLocal.length) warn(`${onlyLocal.length} local-only capabilities were overwritten.`);
593
- console.log();
594
- }
595
-
596
- // Also pull session memory if --memory flag set
597
- if (args.includes("--memory")) {
598
- if (!jsonMode) info("Pulling session memory...");
599
- await pullMemory(args, infernoDir, config, jsonMode);
600
- }
601
- } catch (err) {
602
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: err.message })); }
603
- else { warn(`Cloud unreachable: ${err.message}`); }
604
- process.exit(1);
605
- }
606
- }
607
-
608
- async function subcmdStatus(args, cwd, infernoDir) {
609
- const jsonMode = args.includes("--json");
610
- const config = readCloudConfig(infernoDir);
611
- const token = getToken(config, args);
612
- const endpoint = getEndpoint(config, args);
613
-
614
- if (!config) {
615
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "Not initialised. Run: infernoflow cloud init" })); }
616
- else { warn("Cloud not configured. Run: infernoflow cloud init"); }
617
- return;
618
- }
619
-
620
- const projectId = config.projectId;
621
- const localContract = readContract(infernoDir);
622
- const localHash = localContract ? contractHash(localContract) : null;
623
- const localCaps = (localContract?.capabilities || []).length;
624
-
625
- if (!jsonMode) header("Cloud status");
626
-
627
- let remoteHash = null;
628
- let remoteCaps = 0;
629
- let reachable = false;
630
-
631
- try {
632
- const resp = await httpsRequest(
633
- "GET",
634
- `${endpoint}/api/projects/${projectId}/contract`,
635
- null,
636
- token
637
- );
638
- if (resp.status === 200 && resp.body?.contract) {
639
- reachable = true;
640
- remoteHash = contractHash(resp.body.contract);
641
- remoteCaps = (resp.body.contract?.capabilities || []).length;
642
- }
643
- } catch {}
644
-
645
- const inSync = localHash === remoteHash;
646
- const pending = fs.existsSync(path.join(infernoDir, ".cloud-pending.json"));
647
-
648
- if (jsonMode) {
649
- console.log(JSON.stringify({
650
- ok: true,
651
- projectId,
652
- endpoint,
653
- reachable,
654
- inSync,
655
- pending,
656
- local: { hash: localHash, capabilities: localCaps },
657
- remote: reachable ? { hash: remoteHash, capabilities: remoteCaps } : null,
658
- }));
659
- return;
660
- }
661
-
662
- console.log(` Project: ${cyan(projectId)}`);
663
- console.log(` Endpoint: ${gray(endpoint)}`);
664
- console.log(` Dashboard: ${cyan(`${endpoint}/p/${projectId}`)}`);
665
- console.log();
666
- console.log(` Local: ${bold(String(localCaps))} capabilities ${gray("(hash: " + (localHash || "none") + ")")}`);
667
-
668
- if (!reachable) {
669
- console.log(` Cloud: ${yellow("unreachable")}`);
670
- } else {
671
- console.log(` Cloud: ${bold(String(remoteCaps))} capabilities ${gray("(hash: " + (remoteHash || "none") + ")")}`);
672
- console.log();
673
- if (inSync) {
674
- console.log(` ${green("✔")} In sync with cloud`);
675
- } else {
676
- console.log(` ${yellow("⚠")} Out of sync — run ${cyan("infernoflow cloud push")} or ${cyan("infernoflow cloud pull")}`);
677
- }
678
- }
679
-
680
- if (pending) {
681
- console.log(` ${yellow("⚠")} Pending push queued (cloud was unreachable last time)`);
682
- }
683
-
684
- console.log();
685
- }
686
-
687
- async function subcmdDashboard(args, cwd, infernoDir) {
688
- const config = readCloudConfig(infernoDir);
689
- const endpoint = getEndpoint(config, args);
690
- const projectId = config?.projectId;
691
- const jsonMode = args.includes("--json");
692
-
693
- if (!projectId) {
694
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: "Run: infernoflow cloud init first" })); }
695
- else { warn("Not configured. Run: infernoflow cloud init first."); }
696
- return;
697
- }
698
-
699
- const url = `${endpoint}/p/${projectId}`;
700
-
701
- if (jsonMode) {
702
- console.log(JSON.stringify({ ok: true, url }));
703
- return;
704
- }
705
-
706
- console.log();
707
- console.log(` ${bold("🔥 infernoflow cloud dashboard")}`);
708
- console.log();
709
- console.log(` ${cyan(url)}`);
710
- console.log();
711
- console.log(` ${gray("Share this URL with your whole team.")}`);
712
- console.log();
713
-
714
- // Try to open in browser
715
- try {
716
- const { execSync } = await import("node:child_process");
717
- const cmd = process.platform === "win32" ? `start "" "${url}"` :
718
- process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
719
- execSync(cmd, { stdio: "ignore" });
720
- } catch {}
721
- }
722
-
723
- // ── Entry point ───────────────────────────────────────────────────────────────
724
-
725
- export async function cloudCommand(rawArgs) {
726
- const args = rawArgs.slice(1);
727
- const subcmd = args[0];
728
- const cwd = process.cwd();
729
- const infernoDir = path.join(cwd, "inferno");
730
-
731
- if (!fs.existsSync(infernoDir)) {
732
- const msg = "inferno/ directory not found. Run: infernoflow init";
733
- if (args.includes("--json")) { console.log(JSON.stringify({ ok: false, error: msg })); }
734
- else { warn(msg); }
735
- process.exit(1);
736
- }
737
-
738
- const subArgs = args.slice(1);
739
-
740
- switch (subcmd) {
741
- case "init":
742
- return subcmdInit(subArgs, cwd, infernoDir);
743
- case "push":
744
- return subcmdPush(subArgs, cwd, infernoDir);
745
- case "pull":
746
- return subcmdPull(subArgs, cwd, infernoDir);
747
- case "status":
748
- return subcmdStatus(subArgs, cwd, infernoDir);
749
- case "dashboard":
750
- return subcmdDashboard(subArgs, cwd, infernoDir);
751
- case "memory":
752
- return subcmdMemory(subArgs, cwd, infernoDir);
753
- default: {
754
- const jsonMode = args.includes("--json");
755
- const msg = `Unknown cloud sub-command: ${subcmd || "(none)"}. Use: init | push | pull | memory | status | dashboard`;
756
- if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
757
- else {
758
- console.log();
759
- console.log(` ${bold("infernoflow cloud")} — hosted contract + memory sync`);
760
- console.log();
761
- console.log(` ${cyan("infernoflow cloud init")} Set up cloud sync for this project`);
762
- console.log(` ${cyan("infernoflow cloud push")} Upload local contract to cloud`);
763
- console.log(` ${cyan("infernoflow cloud push --memory")} Also push sessions.jsonl`);
764
- console.log(` ${cyan("infernoflow cloud pull")} Download latest contract from cloud`);
765
- console.log(` ${cyan("infernoflow cloud pull --memory")} Also pull + merge session memory`);
766
- console.log(` ${cyan("infernoflow cloud memory push/pull")} Session memory only`);
767
- console.log(` ${cyan("infernoflow cloud status")} Compare local vs cloud`);
768
- console.log(` ${cyan("infernoflow cloud dashboard")} Open hosted dashboard in browser`);
769
- console.log();
770
- }
771
- }
772
- }
773
- }
1
+ import*as j from"node:fs";import*as O from"node:path";import*as K from"node:https";import*as W from"node:http";import*as M from"node:crypto";import{header as C,ok as E,warn as p,info as k,done as T,bold as m,cyan as h,gray as w,green as A,red as z,yellow as I}from"../ui/output.mjs";const Q="https://cloud.infernoflow.dev",H=".cloud.json";function R(e){const r=O.join(e,H);if(!j.existsSync(r))return null;try{return JSON.parse(j.readFileSync(r,"utf8"))}catch{return null}}function V(e,r){const t=O.join(e,H);j.writeFileSync(t,JSON.stringify(r,null,2)+`
2
+ `)}function P(e,r){const t=r.indexOf("--token");return t!==-1?r[t+1]:process.env.INFERNOFLOW_TOKEN||e?.token||null}function b(e,r){const t=r.indexOf("--endpoint");return t!==-1?r[t+1]:process.env.INFERNOFLOW_ENDPOINT||e?.endpoint||Q}function J(e,r,t,o){return new Promise((n,f)=>{const i=new URL(r),u=i.protocol==="https:",g=u?K:W,s=t?JSON.stringify(t):null,a={hostname:i.hostname,port:i.port||(u?443:80),path:i.pathname+(i.search||""),method:e,headers:{"Content-Type":"application/json",Accept:"application/json","User-Agent":"infernoflow-cli",...o?{Authorization:`Bearer ${o}`}:{},...s?{"Content-Length":Buffer.byteLength(s)}:{}}},c=g.request(a,l=>{let d="";l.on("data",$=>d+=$),l.on("end",()=>{try{n({status:l.statusCode,body:JSON.parse(d)})}catch{n({status:l.statusCode,body:d})}})});c.on("error",f),s&&c.write(s),c.end()})}function L(e){const r=["contract.json","capabilities.json"];for(const t of r){const o=O.join(e,t);if(j.existsSync(o))try{return JSON.parse(j.readFileSync(o,"utf8"))}catch{}}return null}function x(e){return M.createHash("sha256").update(JSON.stringify(e)).digest("hex").slice(0,12)}const D="sessions.jsonl";function U(e){const r=O.join(e,D);return j.existsSync(r)?j.readFileSync(r,"utf8").split(`
3
+ `).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean):[]}function X(e,r){const t=O.join(e,D);j.writeFileSync(t,r.map(o=>JSON.stringify(o)).join(`
4
+ `)+`
5
+ `,"utf8")}function v(e){return M.createHash("sha256").update(JSON.stringify(e.map(r=>r.ts+r.summary))).digest("hex").slice(0,12)}function Y(e,r){const t=new Set(e.map(n=>`${n.ts}|${n.summary}`)),o=[...e];for(const n of r)t.has(`${n.ts}|${n.summary}`)||o.push(n);return o.sort((n,f)=>n.ts.localeCompare(f.ts))}async function _(e,r,t,o=!1){const n=e.includes("--json"),f=e.includes("--dry-run"),i=P(t,e),u=b(t,e),g=t?.projectId;if(!i||!g){const c="No token/project found. Run: infernoflow cloud init";return n?console.log(JSON.stringify({ok:!1,error:c})):o||p(c),{ok:!1}}const s=U(r);if(!s.length)return!o&&!n&&k("No session memory to push (inferno/sessions.jsonl is empty)."),{ok:!0,entries:0};const a=v(s);if(f)return n?console.log(JSON.stringify({ok:!0,dryRun:!0,entries:s.length,hash:a})):o||k(`Dry run \u2014 would push ${m(String(s.length))} memory entries (hash: ${a})`),{ok:!0,dryRun:!0};try{const c=await J("PUT",`${u}/api/projects/${g}/memory`,{entries:s,hash:a,pushedAt:new Date().toISOString()},i),l=c.status===200||c.status===201||c.status===204;return n?console.log(JSON.stringify({ok:l,entries:s.length,hash:a})):o||(l?E(`Pushed ${m(String(s.length))} memory entries`):p(`Cloud returned ${c.status}`)),{ok:l,entries:s.length}}catch(c){return n?console.log(JSON.stringify({ok:!1,error:c.message})):o||p(`Memory push failed: ${c.message}`),{ok:!1}}}async function B(e,r,t,o=!1){const n=e.includes("--json"),f=e.includes("--dry-run"),i=P(t,e),u=b(t,e),g=t?.projectId,s=e.includes("--force")||e.includes("-f");if(!i||!g){const a="No token/project found. Run: infernoflow cloud init";return n?console.log(JSON.stringify({ok:!1,error:a})):o||p(a),{ok:!1}}try{const a=await J("GET",`${u}/api/projects/${g}/memory`,null,i);if(a.status!==200){const S=`Cloud returned ${a.status}`;return n?console.log(JSON.stringify({ok:!1,error:S})):o||p(S),{ok:!1}}const c=a.body?.entries;if(!c||!c.length)return!o&&!n&&k("No session memory in cloud yet. Push first."),{ok:!0,entries:0};const l=U(r),d=s?c:Y(l,c),$=d.length-l.length;return f?(n?console.log(JSON.stringify({ok:!0,dryRun:!0,remote:c.length,local:l.length,merged:d.length})):o||k(`Dry run \u2014 would merge ${m(String(c.length))} remote + ${m(String(l.length))} local = ${m(String(d.length))} entries`),{ok:!0,dryRun:!0}):(X(r,d),n?console.log(JSON.stringify({ok:!0,remote:c.length,local:l.length,merged:d.length,newEntries:$})):o||E(`Merged ${m(String(c.length))} remote entries \u2192 ${m(String(d.length))} total (${$} new)`),{ok:!0,entries:d.length})}catch(a){return n?console.log(JSON.stringify({ok:!1,error:a.message})):o||p(`Memory pull failed: ${a.message}`),{ok:!1}}}async function Z(e,r,t){const o=e.includes("--json"),n=R(t),f=P(n,e),i=b(n,e),u=e[0],g=e.slice(1);if(u==="push")return o||C("Pushing session memory to cloud"),_(g,t,n);if(u==="pull")return o||C("Pulling session memory from cloud"),B(g,t,n);if(u==="status"||!u){const s=U(t),a=n?.projectId;if(!n||!f){o?console.log(JSON.stringify({ok:!1,error:"Not initialised. Run: infernoflow cloud init"})):p("Cloud not configured. Run: infernoflow cloud init");return}let c=null,l=null,d=!1;try{const S=await J("GET",`${i}/api/projects/${a}/memory`,null,f);S.status===200&&S.body?.entries&&(d=!0,c=S.body.entries.length,l=v(S.body.entries))}catch{}const $=s.length?v(s):null;if(o){console.log(JSON.stringify({ok:!0,local:{entries:s.length,hash:$},remote:d?{entries:c,hash:l}:null,reachable:d,inSync:$===l}));return}console.log(),console.log(` ${m("infernoflow cloud memory status")}`),console.log(),console.log(` Local: ${m(String(s.length))} entries ${w("(hash: "+($||"none")+")")}`),d?(console.log(` Cloud: ${m(String(c))} entries ${w("(hash: "+(l||"none")+")")}`),console.log($===l?`
6
+ ${A("\u2714")} Memory in sync`:`
7
+ ${I("\u26A0")} Out of sync \u2014 run ${h("infernoflow cloud memory push")} or ${h("infernoflow cloud memory pull")}`)):console.log(` Cloud: ${I("unreachable")}`),console.log();return}console.log(),console.log(` ${m("infernoflow cloud memory")} \u2014 session memory sync`),console.log(),console.log(` ${h("infernoflow cloud memory push")} Upload sessions.jsonl to cloud`),console.log(` ${h("infernoflow cloud memory pull")} Download + merge remote memory`),console.log(` ${h("infernoflow cloud memory status")} Compare local vs remote`),console.log()}async function q(e,r,t){const o=e.includes("--json"),n=b(null,e),f=e.includes("--dry-run"),i=R(t);if(i&&!e.includes("--force")&&!e.includes("-f")){o?console.log(JSON.stringify({ok:!1,error:"Already initialised. Use --force to overwrite.",config:i})):(p("Cloud already configured for this project."),console.log(` Token: ${w(i.token)}`),console.log(` Endpoint: ${w(i.endpoint)}`),console.log(` Project: ${w(i.projectId)}`),console.log(),k("Use --force to generate a new token."));return}const u=M.randomBytes(8).toString("hex"),g=M.randomBytes(24).toString("base64url"),s={projectId:u,token:g,endpoint:n,createdAt:new Date().toISOString()};if(f){o?console.log(JSON.stringify({ok:!0,dryRun:!0,config:s})):(k("Dry run \u2014 would write inferno/.cloud.json:"),console.log(" "+JSON.stringify(s,null,2).split(`
8
+ `).join(`
9
+ `)));return}o||C("Initialising infernoflow cloud");try{const a=await J("POST",`${n}/api/projects`,{projectId:u},null);(a.status===200||a.status===201)&&(o||E("Project registered on cloud"))}catch{o||k("Cloud endpoint unreachable \u2014 saved config locally (will connect on first push)")}V(t,s),o?console.log(JSON.stringify({ok:!0,projectId:u,endpoint:n})):(T("Cloud configured!"),console.log(),console.log(` Project ID: ${h(u)}`),console.log(` Endpoint: ${w(n)}`),console.log(` Token: ${w(g.slice(0,8)+"\u2026")} (stored in inferno/.cloud.json)`),console.log(),console.log(` ${w("Share the dashboard:")} ${h(`${n}/p/${u}`)}`),console.log(),console.log(` ${I("\u26A0")} Add inferno/.cloud.json to .gitignore to protect your token!`),console.log(` ${w("echo 'inferno/.cloud.json' >> .gitignore")}`),console.log())}async function oo(e,r,t){const o=e.includes("--json"),n=e.includes("--dry-run"),f=R(t),i=P(f,e),u=b(f,e);if(!i){const l="No token found. Run: infernoflow cloud init";o?console.log(JSON.stringify({ok:!1,error:l})):p(l),process.exit(1)}const g=L(t);if(!g){const l="No contract.json found. Run: infernoflow init";o?console.log(JSON.stringify({ok:!1,error:l})):p(l),process.exit(1)}const s=f?.projectId||"unknown",a=x(g),c=(g.capabilities||[]).length;if(n){o?console.log(JSON.stringify({ok:!0,dryRun:!0,projectId:s,hash:a,capabilities:c})):k(`Dry run \u2014 would push ${m(String(c))} capabilities (hash: ${a}) to ${u}`);return}o||C("Pushing contract to cloud");try{const l=await J("PUT",`${u}/api/projects/${s}/contract`,{contract:g,hash:a,pushedAt:new Date().toISOString()},i);if(l.status===200||l.status===201||l.status===204)o?console.log(JSON.stringify({ok:!0,projectId:s,hash:a,capabilities:c})):(T(`Pushed ${m(String(c))} capabilities`),console.log(` ${w("Dashboard:")} ${h(`${u}/p/${s}`)}`),console.log()),e.includes("--memory")&&(o||k("Pushing session memory..."),await _(e,t,f,o),o||E("Session memory pushed"));else{const d=`Cloud returned ${l.status}`;o?console.log(JSON.stringify({ok:!1,error:d,status:l.status})):p(d),process.exit(1)}}catch(l){const d=O.join(t,".cloud-pending.json");j.writeFileSync(d,JSON.stringify({hash:a,pendingAt:new Date().toISOString()})),o?console.log(JSON.stringify({ok:!1,error:l.message,pending:!0})):(p("Cloud unreachable \u2014 push queued locally."),k("Changes will sync automatically on next successful connection."))}}async function eo(e,r,t){const o=e.includes("--json"),n=e.includes("--dry-run"),f=R(t),i=P(f,e),u=b(f,e);if(!i){const s="No token found. Run: infernoflow cloud init";o?console.log(JSON.stringify({ok:!1,error:s})):p(s),process.exit(1)}const g=f?.projectId||"unknown";o||C("Pulling contract from cloud");try{const s=await J("GET",`${u}/api/projects/${g}/contract`,null,i);if(s.status!==200){const y=`Cloud returned ${s.status}`;o?console.log(JSON.stringify({ok:!1,error:y})):p(y),process.exit(1)}const a=s.body?.contract,c=L(t);if(!a){const y="No contract found on cloud. Push first.";o?console.log(JSON.stringify({ok:!1,error:y})):p(y);return}const l=(c?.capabilities||[]).map(y=>typeof y=="string"?y:y.id),d=(a.capabilities||[]).map(y=>typeof y=="string"?y:y.id),$=new Set(l),S=new Set(d),N=l.filter(y=>!S.has(y)),F=d.filter(y=>!$.has(y));if(N.length>0&&F.length>0&&(o?console.log(JSON.stringify({ok:!1,conflict:!0,onlyLocal:N,onlyRemote:F})):(p("Diverged contracts detected:"),N.forEach(y=>console.log(` ${z("-")} local-only: ${y}`)),F.forEach(y=>console.log(` ${A("+")} remote-only: ${y}`)),console.log(),p("Merge manually or use --force to overwrite local with remote.")),!e.includes("--force")&&!e.includes("-f")))return;if(n){o?console.log(JSON.stringify({ok:!0,dryRun:!0,capabilities:d.length,hash:x(a)})):k(`Dry run \u2014 would write ${m(String(d.length))} capabilities from cloud`);return}const G=O.join(t,"contract.json");j.writeFileSync(G,JSON.stringify(a,null,2)+`
10
+ `),o?console.log(JSON.stringify({ok:!0,capabilities:d.length,hash:x(a)})):(T(`Pulled ${m(String(d.length))} capabilities from cloud`),N.length&&p(`${N.length} local-only capabilities were overwritten.`),console.log()),e.includes("--memory")&&(o||k("Pulling session memory..."),await B(e,t,f,o))}catch(s){o?console.log(JSON.stringify({ok:!1,error:s.message})):p(`Cloud unreachable: ${s.message}`),process.exit(1)}}async function no(e,r,t){const o=e.includes("--json"),n=R(t),f=P(n,e),i=b(n,e);if(!n){o?console.log(JSON.stringify({ok:!1,error:"Not initialised. Run: infernoflow cloud init"})):p("Cloud not configured. Run: infernoflow cloud init");return}const u=n.projectId,g=L(t),s=g?x(g):null,a=(g?.capabilities||[]).length;o||C("Cloud status");let c=null,l=0,d=!1;try{const N=await J("GET",`${i}/api/projects/${u}/contract`,null,f);N.status===200&&N.body?.contract&&(d=!0,c=x(N.body.contract),l=(N.body.contract?.capabilities||[]).length)}catch{}const $=s===c,S=j.existsSync(O.join(t,".cloud-pending.json"));if(o){console.log(JSON.stringify({ok:!0,projectId:u,endpoint:i,reachable:d,inSync:$,pending:S,local:{hash:s,capabilities:a},remote:d?{hash:c,capabilities:l}:null}));return}console.log(` Project: ${h(u)}`),console.log(` Endpoint: ${w(i)}`),console.log(` Dashboard: ${h(`${i}/p/${u}`)}`),console.log(),console.log(` Local: ${m(String(a))} capabilities ${w("(hash: "+(s||"none")+")")}`),d?(console.log(` Cloud: ${m(String(l))} capabilities ${w("(hash: "+(c||"none")+")")}`),console.log(),console.log($?` ${A("\u2714")} In sync with cloud`:` ${I("\u26A0")} Out of sync \u2014 run ${h("infernoflow cloud push")} or ${h("infernoflow cloud pull")}`)):console.log(` Cloud: ${I("unreachable")}`),S&&console.log(` ${I("\u26A0")} Pending push queued (cloud was unreachable last time)`),console.log()}async function to(e,r,t){const o=R(t),n=b(o,e),f=o?.projectId,i=e.includes("--json");if(!f){i?console.log(JSON.stringify({ok:!1,error:"Run: infernoflow cloud init first"})):p("Not configured. Run: infernoflow cloud init first.");return}const u=`${n}/p/${f}`;if(i){console.log(JSON.stringify({ok:!0,url:u}));return}console.log(),console.log(` ${m("\u{1F525} infernoflow cloud dashboard")}`),console.log(),console.log(` ${h(u)}`),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 "" "${u}"`:process.platform==="darwin"?`open "${u}"`:`xdg-open "${u}"`;g(s,{stdio:"ignore"})}catch{}}async function lo(e){const r=e.slice(1),t=r[0],o=process.cwd(),n=O.join(o,"inferno");if(!j.existsSync(n)){const i="inferno/ directory not found. Run: infernoflow init";r.includes("--json")?console.log(JSON.stringify({ok:!1,error:i})):p(i),process.exit(1)}const f=r.slice(1);switch(t){case"init":return q(f,o,n);case"push":return oo(f,o,n);case"pull":return eo(f,o,n);case"status":return no(f,o,n);case"dashboard":return to(f,o,n);case"memory":return Z(f,o,n);default:{const i=r.includes("--json"),u=`Unknown cloud sub-command: ${t||"(none)"}. Use: init | push | pull | memory | status | dashboard`;i?console.log(JSON.stringify({ok:!1,error:u})):(console.log(),console.log(` ${m("infernoflow cloud")} \u2014 hosted contract + memory sync`),console.log(),console.log(` ${h("infernoflow cloud init")} Set up cloud sync for this project`),console.log(` ${h("infernoflow cloud push")} Upload local contract to cloud`),console.log(` ${h("infernoflow cloud push --memory")} Also push sessions.jsonl`),console.log(` ${h("infernoflow cloud pull")} Download latest contract from cloud`),console.log(` ${h("infernoflow cloud pull --memory")} Also pull + merge session memory`),console.log(` ${h("infernoflow cloud memory push/pull")} Session memory only`),console.log(` ${h("infernoflow cloud status")} Compare local vs cloud`),console.log(` ${h("infernoflow cloud dashboard")} Open hosted dashboard in browser`),console.log())}}}export{lo as cloudCommand};