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.
- package/CHANGELOG.md +125 -0
- package/dist/bin/infernoflow.mjs +29 -277
- package/dist/lib/adopters/angular.mjs +1 -128
- package/dist/lib/adopters/css.mjs +1 -111
- package/dist/lib/adopters/react.mjs +1 -104
- package/dist/lib/ai/ideDetection.mjs +1 -31
- package/dist/lib/ai/localProvider.mjs +1 -88
- package/dist/lib/ai/providerRouter.mjs +2 -295
- package/dist/lib/commands/adopt.mjs +20 -869
- package/dist/lib/commands/adoptWizard.mjs +9 -320
- package/dist/lib/commands/agent.mjs +5 -191
- package/dist/lib/commands/ai.mjs +2 -407
- package/dist/lib/commands/ask.mjs +4 -299
- package/dist/lib/commands/audit.mjs +13 -300
- package/dist/lib/commands/changelog.mjs +26 -594
- package/dist/lib/commands/check.mjs +3 -184
- package/dist/lib/commands/ci.mjs +3 -208
- package/dist/lib/commands/claudeMd.mjs +30 -135
- package/dist/lib/commands/cloud.mjs +10 -773
- package/dist/lib/commands/context.mjs +34 -346
- package/dist/lib/commands/coverage.mjs +2 -282
- package/dist/lib/commands/dashboard.mjs +123 -635
- package/dist/lib/commands/demo.mjs +8 -465
- package/dist/lib/commands/diff.mjs +5 -274
- package/dist/lib/commands/docGate.mjs +2 -81
- package/dist/lib/commands/doctor.mjs +3 -321
- package/dist/lib/commands/explain.mjs +8 -438
- package/dist/lib/commands/export.mjs +10 -239
- package/dist/lib/commands/feedback.mjs +12 -216
- package/dist/lib/commands/generateSkills.mjs +38 -163
- package/dist/lib/commands/graph.mjs +11 -378
- package/dist/lib/commands/health.mjs +2 -309
- package/dist/lib/commands/impact.mjs +2 -325
- package/dist/lib/commands/implement.mjs +7 -103
- package/dist/lib/commands/init.mjs +45 -631
- package/dist/lib/commands/installCursorHooks.mjs +1 -36
- package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
- package/dist/lib/commands/link.mjs +2 -342
- package/dist/lib/commands/log.mjs +18 -248
- package/dist/lib/commands/monorepo.mjs +4 -428
- package/dist/lib/commands/notify.mjs +4 -258
- package/dist/lib/commands/onboard.mjs +4 -296
- package/dist/lib/commands/prComment.mjs +2 -361
- package/dist/lib/commands/prImpact.mjs +2 -157
- package/dist/lib/commands/publish.mjs +15 -316
- package/dist/lib/commands/recap.mjs +6 -380
- package/dist/lib/commands/report.mjs +28 -272
- package/dist/lib/commands/review.mjs +9 -223
- package/dist/lib/commands/run.mjs +8 -336
- package/dist/lib/commands/scaffold.mjs +54 -419
- package/dist/lib/commands/scan.mjs +11 -1118
- package/dist/lib/commands/scout.mjs +2 -291
- package/dist/lib/commands/setup.mjs +5 -310
- package/dist/lib/commands/share.mjs +13 -196
- package/dist/lib/commands/snapshot.mjs +3 -383
- package/dist/lib/commands/stability.mjs +2 -293
- package/dist/lib/commands/stats.mjs +5 -402
- package/dist/lib/commands/status.mjs +4 -172
- package/dist/lib/commands/suggest.mjs +21 -563
- package/dist/lib/commands/switch.mjs +13 -517
- package/dist/lib/commands/syncAuto.mjs +1 -96
- package/dist/lib/commands/synthesize.mjs +10 -228
- package/dist/lib/commands/teamSync.mjs +2 -388
- package/dist/lib/commands/test.mjs +6 -363
- package/dist/lib/commands/theme.mjs +18 -195
- package/dist/lib/commands/uninstall.mjs +13 -406
- package/dist/lib/commands/upgrade.mjs +20 -153
- package/dist/lib/commands/version.mjs +2 -282
- package/dist/lib/commands/vibe.mjs +7 -357
- package/dist/lib/commands/watch.mjs +4 -203
- package/dist/lib/commands/why.mjs +4 -358
- package/dist/lib/cursorHooksInstall.mjs +1 -60
- package/dist/lib/draftToolingInstall.mjs +7 -68
- package/dist/lib/git/detect-drift.mjs +4 -208
- package/dist/lib/learning/adapt.mjs +6 -101
- package/dist/lib/learning/observe.mjs +1 -119
- package/dist/lib/learning/patternDetector.mjs +1 -298
- package/dist/lib/learning/profile.mjs +2 -279
- package/dist/lib/learning/skillSynthesizer.mjs +24 -145
- package/dist/lib/telemetry.mjs +19 -269
- package/dist/lib/templates/index.mjs +1 -131
- package/dist/lib/theme/scanner.mjs +4 -343
- package/dist/lib/ui/errors.mjs +1 -142
- package/dist/lib/ui/output.mjs +6 -95
- package/dist/lib/ui/prompts.mjs +6 -147
- package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
- package/package.json +2 -4
- package/scripts/postinstall.js +2 -2
|
@@ -1,773 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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};
|