infernoflow 0.37.1 → 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 +64 -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 -520
- 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,228 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* infernoflow synthesize --threshold 2 — surface patterns seen 2+ times
|
|
12
|
-
* infernoflow synthesize --watch — re-run every 60s, surface new candidates
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import * as fs from "node:fs";
|
|
16
|
-
import * as path from "node:path";
|
|
17
|
-
import * as readline from "node:readline";
|
|
18
|
-
import { header, ok, warn, info, done, bold, cyan, yellow, green, gray } from "../ui/output.mjs";
|
|
19
|
-
import { readProfile, writeProfile } from "../learning/profile.mjs";
|
|
20
|
-
import { detectPatterns, mergeCandidates, pendingCandidates } from "../learning/patternDetector.mjs";
|
|
21
|
-
import { synthesizeCandidate } from "../learning/skillSynthesizer.mjs";
|
|
22
|
-
import { observeCommandStart } from "../learning/observe.mjs";
|
|
23
|
-
|
|
24
|
-
function findInfernoDir(cwd) {
|
|
25
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
26
|
-
if (!fs.existsSync(infernoDir)) return null;
|
|
27
|
-
return infernoDir;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function ask(rl, question) {
|
|
31
|
-
return new Promise(resolve => rl.question(question, a => resolve(a.trim().toLowerCase())));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function renderCandidate(c, index) {
|
|
35
|
-
const type = c.type === "agent" ? cyan("agent") : green("skill");
|
|
36
|
-
const conf = Math.round(c.confidence * 100);
|
|
37
|
-
const confStr = conf >= 80 ? green(`${conf}%`) : conf >= 60 ? yellow(`${conf}%`) : gray(`${conf}%`);
|
|
38
|
-
const freq = gray(`seen ${c.frequency}×`);
|
|
39
|
-
|
|
40
|
-
console.log(`\n ${bold(`[${index + 1}]`)} ${type} ${bold(c.name)} ${confStr} confidence ${freq}`);
|
|
41
|
-
console.log(` Pattern: ${cyan(c.trigger)}`);
|
|
42
|
-
if (c.steps) console.log(` Steps: ${c.steps.join(" → ")}`);
|
|
43
|
-
if (c.examples?.length) {
|
|
44
|
-
console.log(` Examples: ${c.examples.slice(0, 2).map(e => `"${e}"`).join(", ")}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function reviewInteractive(cwd, infernoDir, profile, candidates) {
|
|
49
|
-
if (candidates.length === 0) {
|
|
50
|
-
info("No new candidates to review.");
|
|
51
|
-
return { approved: 0, rejected: 0 };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
55
|
-
let approved = 0, rejected = 0;
|
|
56
|
-
|
|
57
|
-
console.log(`\n ${bold("Candidates found:")} ${candidates.length}\n`);
|
|
58
|
-
console.log(gray(" For each: [y] approve [n] reject [s] skip [q] quit\n"));
|
|
59
|
-
|
|
60
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
61
|
-
const c = candidates[i];
|
|
62
|
-
renderCandidate(c, i);
|
|
63
|
-
|
|
64
|
-
const ans = await ask(rl, `\n Approve? [y/n/s/q]: `);
|
|
65
|
-
|
|
66
|
-
if (ans === "q") break;
|
|
67
|
-
if (ans === "s") continue;
|
|
68
|
-
|
|
69
|
-
const all = [...(profile.agentCandidates || []), ...(profile.skillCandidates || [])];
|
|
70
|
-
const entry = all.find(x => x.id === c.id);
|
|
71
|
-
if (!entry) continue;
|
|
72
|
-
|
|
73
|
-
if (ans === "y") {
|
|
74
|
-
entry.status = "approved";
|
|
75
|
-
try {
|
|
76
|
-
const { filePaths } = synthesizeCandidate(cwd, infernoDir, c);
|
|
77
|
-
const approvedList = c.type === "agent" ? "approvedAgents" : "approvedSkills";
|
|
78
|
-
if (!profile[approvedList]) profile[approvedList] = [];
|
|
79
|
-
profile[approvedList].push({
|
|
80
|
-
id: c.id,
|
|
81
|
-
name: c.name,
|
|
82
|
-
filePaths,
|
|
83
|
-
approvedAt: new Date().toISOString(),
|
|
84
|
-
});
|
|
85
|
-
ok(` ${c.type === "agent" ? "Agent" : "Skill"} created:`);
|
|
86
|
-
for (const fp of filePaths) console.log(` ${green("→")} ${fp}`);
|
|
87
|
-
approved++;
|
|
88
|
-
} catch (err) {
|
|
89
|
-
warn(` Could not write files: ${err.message}`);
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
entry.status = "rejected";
|
|
93
|
-
rejected++;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
rl.close();
|
|
98
|
-
return { approved, rejected };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function autoApprove(cwd, infernoDir, profile, candidates, threshold = 0.8) {
|
|
102
|
-
let approved = 0;
|
|
103
|
-
for (const c of candidates) {
|
|
104
|
-
if (c.confidence < threshold) continue;
|
|
105
|
-
|
|
106
|
-
const all = [...(profile.agentCandidates || []), ...(profile.skillCandidates || [])];
|
|
107
|
-
const entry = all.find(x => x.id === c.id);
|
|
108
|
-
if (!entry) continue;
|
|
109
|
-
|
|
110
|
-
entry.status = "approved";
|
|
111
|
-
try {
|
|
112
|
-
const { filePaths } = synthesizeCandidate(cwd, infernoDir, c);
|
|
113
|
-
const approvedList = c.type === "agent" ? "approvedAgents" : "approvedSkills";
|
|
114
|
-
if (!profile[approvedList]) profile[approvedList] = [];
|
|
115
|
-
profile[approvedList].push({
|
|
116
|
-
id: c.id,
|
|
117
|
-
name: c.name,
|
|
118
|
-
filePaths,
|
|
119
|
-
approvedAt: new Date().toISOString(),
|
|
120
|
-
});
|
|
121
|
-
ok(`Auto-approved ${c.type}: ${bold(c.name)} (${Math.round(c.confidence * 100)}% confidence)`);
|
|
122
|
-
for (const fp of filePaths) console.log(` ${green("→")} ${fp}`);
|
|
123
|
-
approved++;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
warn(`Could not write ${c.name}: ${err.message}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return approved;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function synthesizeCommand(args) {
|
|
132
|
-
const cwd = process.cwd();
|
|
133
|
-
const isAuto = args.includes("--auto");
|
|
134
|
-
const isJson = args.includes("--json");
|
|
135
|
-
const isWatch = args.includes("--watch");
|
|
136
|
-
const threshold = (() => {
|
|
137
|
-
const idx = args.indexOf("--threshold");
|
|
138
|
-
return idx !== -1 ? Number(args[idx + 1]) || 3 : 3;
|
|
139
|
-
})();
|
|
140
|
-
|
|
141
|
-
const infernoDir = findInfernoDir(cwd);
|
|
142
|
-
if (!infernoDir) {
|
|
143
|
-
if (isJson) {
|
|
144
|
-
process.stdout.write(JSON.stringify({ ok: false, error: "inferno/ not found" }) + "\n");
|
|
145
|
-
} else {
|
|
146
|
-
warn("inferno/ not found — run infernoflow init first");
|
|
147
|
-
}
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
observeCommandStart(infernoDir, "synthesize");
|
|
152
|
-
|
|
153
|
-
async function runOnce() {
|
|
154
|
-
const profile = readProfile(infernoDir);
|
|
155
|
-
|
|
156
|
-
// ── Detect patterns ──────────────────────────────────────────────────────
|
|
157
|
-
const { agentCandidates, skillCandidates } = detectPatterns(profile, { minFreq: threshold });
|
|
158
|
-
mergeCandidates(profile, { agentCandidates, skillCandidates });
|
|
159
|
-
|
|
160
|
-
const pending = pendingCandidates(profile);
|
|
161
|
-
|
|
162
|
-
// ── JSON mode ────────────────────────────────────────────────────────────
|
|
163
|
-
if (isJson) {
|
|
164
|
-
writeProfile(infernoDir, profile);
|
|
165
|
-
process.stdout.write(JSON.stringify({
|
|
166
|
-
ok: true,
|
|
167
|
-
pendingCount: pending.length,
|
|
168
|
-
candidates: pending,
|
|
169
|
-
sessions: profile.recentSessions?.length || 0,
|
|
170
|
-
}, null, 2) + "\n");
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
header("infernoflow synthesize");
|
|
175
|
-
info(`Sessions analyzed: ${bold(String(profile.recentSessions?.length || 0))}`);
|
|
176
|
-
info(`Minimum frequency: ${bold(String(threshold))}×`);
|
|
177
|
-
|
|
178
|
-
if (pending.length === 0) {
|
|
179
|
-
console.log();
|
|
180
|
-
if ((profile.recentSessions?.length || 0) < threshold) {
|
|
181
|
-
warn(`Not enough session data yet — need at least ${threshold} sessions with similar commands.`);
|
|
182
|
-
warn(`Keep using infernoflow and run synthesize again soon.`);
|
|
183
|
-
} else {
|
|
184
|
-
ok("No new patterns detected — your workflow is already well captured.");
|
|
185
|
-
}
|
|
186
|
-
writeProfile(infernoDir, profile);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
info(`New patterns detected: ${bold(String(pending.length))}`);
|
|
191
|
-
|
|
192
|
-
// ── Auto mode ────────────────────────────────────────────────────────────
|
|
193
|
-
let approved;
|
|
194
|
-
if (isAuto) {
|
|
195
|
-
info(`Auto-approving candidates with ≥80% confidence...`);
|
|
196
|
-
approved = autoApprove(cwd, infernoDir, profile, pending);
|
|
197
|
-
} else {
|
|
198
|
-
const result = await reviewInteractive(cwd, infernoDir, profile, pending);
|
|
199
|
-
approved = result.approved;
|
|
200
|
-
if (result.rejected > 0) info(`Rejected: ${result.rejected}`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
writeProfile(infernoDir, profile);
|
|
204
|
-
|
|
205
|
-
if (approved > 0) {
|
|
206
|
-
console.log();
|
|
207
|
-
done(`${approved} skill${approved !== 1 ? "s/agents" : "/agent"} synthesized`);
|
|
208
|
-
console.log(`\n ${bold("What was created:")}`);
|
|
209
|
-
for (const item of [...(profile.approvedSkills || []), ...(profile.approvedAgents || [])].slice(-approved)) {
|
|
210
|
-
const type = profile.approvedAgents?.find(a => a.id === item.id) ? "agent" : "skill";
|
|
211
|
-
console.log(` ${green("✔")} ${type}: ${bold(item.name)}`);
|
|
212
|
-
for (const fp of item.filePaths || []) console.log(` ${gray(fp)}`);
|
|
213
|
-
}
|
|
214
|
-
console.log(`\n ${bold("Next:")} Run ${cyan("infernoflow agent list")} to see your agents`);
|
|
215
|
-
console.log(` or ${cyan("infernoflow agent run <name>")} to execute one`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (isWatch) {
|
|
220
|
-
info("Watching for new patterns — press Ctrl+C to stop");
|
|
221
|
-
const INTERVAL = 60_000;
|
|
222
|
-
await runOnce();
|
|
223
|
-
setInterval(runOnce, INTERVAL);
|
|
224
|
-
await new Promise(() => {}); // keep alive
|
|
225
|
-
} else {
|
|
226
|
-
await runOnce();
|
|
227
|
-
}
|
|
228
|
-
}
|
|
1
|
+
import*as j from"node:fs";import*as x from"node:path";import*as q from"node:readline";import{header as N,ok as C,warn as $,info as g,done as P,bold as f,cyan as w,yellow as z,green as h,gray as y}from"../ui/output.mjs";import{readProfile as I,writeProfile as k}from"../learning/profile.mjs";import{detectPatterns as O,mergeCandidates as D,pendingCandidates as b}from"../learning/patternDetector.mjs";import{synthesizeCandidate as A}from"../learning/skillSynthesizer.mjs";import{observeCommandStart as L}from"../learning/observe.mjs";function J(t){const i=x.join(t,"inferno");return j.existsSync(i)?i:null}function M(t,i){return new Promise(o=>t.question(i,s=>o(s.trim().toLowerCase())))}function R(t,i){const o=t.type==="agent"?w("agent"):h("skill"),s=Math.round(t.confidence*100),u=s>=80?h(`${s}%`):s>=60?z(`${s}%`):y(`${s}%`),a=y(`seen ${t.frequency}\xD7`);console.log(`
|
|
2
|
+
${f(`[${i+1}]`)} ${o} ${f(t.name)} ${u} confidence ${a}`),console.log(` Pattern: ${w(t.trigger)}`),t.steps&&console.log(` Steps: ${t.steps.join(" \u2192 ")}`),t.examples?.length&&console.log(` Examples: ${t.examples.slice(0,2).map(n=>`"${n}"`).join(", ")}`)}async function W(t,i,o,s){if(s.length===0)return g("No new candidates to review."),{approved:0,rejected:0};const u=q.createInterface({input:process.stdin,output:process.stdout});let a=0,n=0;console.log(`
|
|
3
|
+
${f("Candidates found:")} ${s.length}
|
|
4
|
+
`),console.log(y(` For each: [y] approve [n] reject [s] skip [q] quit
|
|
5
|
+
`));for(let p=0;p<s.length;p++){const e=s[p];R(e,p);const c=await M(u,`
|
|
6
|
+
Approve? [y/n/s/q]: `);if(c==="q")break;if(c==="s")continue;const l=[...o.agentCandidates||[],...o.skillCandidates||[]].find(r=>r.id===e.id);if(l)if(c==="y"){l.status="approved";try{const{filePaths:r}=A(t,i,e),d=e.type==="agent"?"approvedAgents":"approvedSkills";o[d]||(o[d]=[]),o[d].push({id:e.id,name:e.name,filePaths:r,approvedAt:new Date().toISOString()}),C(` ${e.type==="agent"?"Agent":"Skill"} created:`);for(const v of r)console.log(` ${h("\u2192")} ${v}`);a++}catch(r){$(` Could not write files: ${r.message}`)}}else l.status="rejected",n++}return u.close(),{approved:a,rejected:n}}function E(t,i,o,s,u=.8){let a=0;for(const n of s){if(n.confidence<u)continue;const e=[...o.agentCandidates||[],...o.skillCandidates||[]].find(c=>c.id===n.id);if(e){e.status="approved";try{const{filePaths:c}=A(t,i,n),m=n.type==="agent"?"approvedAgents":"approvedSkills";o[m]||(o[m]=[]),o[m].push({id:n.id,name:n.name,filePaths:c,approvedAt:new Date().toISOString()}),C(`Auto-approved ${n.type}: ${f(n.name)} (${Math.round(n.confidence*100)}% confidence)`);for(const l of c)console.log(` ${h("\u2192")} ${l}`);a++}catch(c){$(`Could not write ${n.name}: ${c.message}`)}}}return a}async function B(t){const i=process.cwd(),o=t.includes("--auto"),s=t.includes("--json"),u=t.includes("--watch"),a=(()=>{const e=t.indexOf("--threshold");return e!==-1&&Number(t[e+1])||3})(),n=J(i);n||(s?process.stdout.write(JSON.stringify({ok:!1,error:"inferno/ not found"})+`
|
|
7
|
+
`):$("inferno/ not found \u2014 run infernoflow init first"),process.exit(1)),L(n,"synthesize");async function p(){const e=I(n),{agentCandidates:c,skillCandidates:m}=O(e,{minFreq:a});D(e,{agentCandidates:c,skillCandidates:m});const l=b(e);if(s){k(n,e),process.stdout.write(JSON.stringify({ok:!0,pendingCount:l.length,candidates:l,sessions:e.recentSessions?.length||0},null,2)+`
|
|
8
|
+
`);return}if(N("infernoflow synthesize"),g(`Sessions analyzed: ${f(String(e.recentSessions?.length||0))}`),g(`Minimum frequency: ${f(String(a))}\xD7`),l.length===0){console.log(),(e.recentSessions?.length||0)<a?($(`Not enough session data yet \u2014 need at least ${a} sessions with similar commands.`),$("Keep using infernoflow and run synthesize again soon.")):C("No new patterns detected \u2014 your workflow is already well captured."),k(n,e);return}g(`New patterns detected: ${f(String(l.length))}`);let r;if(o)g("Auto-approving candidates with \u226580% confidence..."),r=E(i,n,e,l);else{const d=await W(i,n,e,l);r=d.approved,d.rejected>0&&g(`Rejected: ${d.rejected}`)}if(k(n,e),r>0){console.log(),P(`${r} skill${r!==1?"s/agents":"/agent"} synthesized`),console.log(`
|
|
9
|
+
${f("What was created:")}`);for(const d of[...e.approvedSkills||[],...e.approvedAgents||[]].slice(-r)){const v=e.approvedAgents?.find(S=>S.id===d.id)?"agent":"skill";console.log(` ${h("\u2714")} ${v}: ${f(d.name)}`);for(const S of d.filePaths||[])console.log(` ${y(S)}`)}console.log(`
|
|
10
|
+
${f("Next:")} Run ${w("infernoflow agent list")} to see your agents`),console.log(` or ${w("infernoflow agent run <name>")} to execute one`)}}if(u){g("Watching for new patterns \u2014 press Ctrl+C to stop");const e=6e4;await p(),setInterval(p,e),await new Promise(()=>{})}else await p()}export{B as synthesizeCommand};
|
|
@@ -1,388 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Shared capability contract sync across a team.
|
|
5
|
-
* Uses a dedicated git branch (`inferno-contracts`) as the source of truth.
|
|
6
|
-
*
|
|
7
|
-
* Sub-commands:
|
|
8
|
-
* infernoflow team-sync push — push local contract to shared branch
|
|
9
|
-
* infernoflow team-sync pull — pull shared contract, detect conflicts
|
|
10
|
-
* infernoflow team-sync status — show diff between local and shared
|
|
11
|
-
* infernoflow team-sync init — create the shared branch if it doesn't exist
|
|
12
|
-
*
|
|
13
|
-
* Flags:
|
|
14
|
-
* --branch <name> Shared branch name (default: inferno-contracts)
|
|
15
|
-
* --remote <name> Git remote (default: origin)
|
|
16
|
-
* --json Machine-readable output
|
|
17
|
-
* --force Overwrite conflicts without prompting
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import * as fs from "node:fs";
|
|
21
|
-
import * as path from "node:path";
|
|
22
|
-
import { execSync } from "node:child_process";
|
|
23
|
-
import { header, ok, warn, info, done, bold, cyan, gray, green, red, yellow } from "../ui/output.mjs";
|
|
24
|
-
|
|
25
|
-
// ── git helpers ───────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
function capture(cmd, cwd) {
|
|
28
|
-
try {
|
|
29
|
-
return execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
30
|
-
} catch { return null; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function run(cmd, cwd) {
|
|
34
|
-
execSync(cmd, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function currentBranch(cwd) {
|
|
38
|
-
return capture("git rev-parse --abbrev-ref HEAD", cwd) || "HEAD";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function currentUser(cwd) {
|
|
42
|
-
return capture("git config user.name", cwd) || capture("git config user.email", cwd) || "unknown";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function hasRemote(remote, cwd) {
|
|
46
|
-
return !!capture(`git remote get-url ${remote}`, cwd);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function branchExistsRemote(remote, branch, cwd) {
|
|
50
|
-
return !!capture(`git ls-remote --heads ${remote} refs/heads/${branch}`, cwd);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── capability helpers ────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
function parseCaps(jsonText) {
|
|
56
|
-
if (!jsonText) return [];
|
|
57
|
-
try {
|
|
58
|
-
const obj = JSON.parse(jsonText);
|
|
59
|
-
const raw = obj.capabilities || [];
|
|
60
|
-
return raw.map(c => typeof c === "string" ? { id: c, title: c } : c);
|
|
61
|
-
} catch { return []; }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function capsToMap(caps) {
|
|
65
|
-
return new Map(caps.map(c => [c.id, c]));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function detectConflicts(local, shared, base) {
|
|
69
|
-
// A conflict occurs when BOTH local and shared changed the same capability
|
|
70
|
-
// since the last sync (base).
|
|
71
|
-
const localMap = capsToMap(local);
|
|
72
|
-
const sharedMap = capsToMap(shared);
|
|
73
|
-
const baseMap = capsToMap(base);
|
|
74
|
-
|
|
75
|
-
const conflicts = [];
|
|
76
|
-
const localOnly = [];
|
|
77
|
-
const sharedOnly = [];
|
|
78
|
-
|
|
79
|
-
const allIds = new Set([...localMap.keys(), ...sharedMap.keys(), ...baseMap.keys()]);
|
|
80
|
-
|
|
81
|
-
for (const id of allIds) {
|
|
82
|
-
const localCap = localMap.get(id);
|
|
83
|
-
const sharedCap = sharedMap.get(id);
|
|
84
|
-
const baseCap = baseMap.get(id);
|
|
85
|
-
|
|
86
|
-
const localChanged = JSON.stringify(localCap) !== JSON.stringify(baseCap);
|
|
87
|
-
const sharedChanged = JSON.stringify(sharedCap) !== JSON.stringify(baseCap);
|
|
88
|
-
|
|
89
|
-
if (localChanged && sharedChanged && JSON.stringify(localCap) !== JSON.stringify(sharedCap)) {
|
|
90
|
-
conflicts.push({ id, local: localCap, shared: sharedCap, base: baseCap });
|
|
91
|
-
} else if (localCap && !sharedCap && !baseCap) {
|
|
92
|
-
localOnly.push(localCap); // added locally, not in shared yet
|
|
93
|
-
} else if (!localCap && sharedCap && !baseCap) {
|
|
94
|
-
sharedOnly.push(sharedCap); // added in shared, not locally yet
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { conflicts, localOnly, sharedOnly };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── shared branch operations ──────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
function readContractFromBranch(remote, branch, cwd) {
|
|
104
|
-
// Fetch the branch first
|
|
105
|
-
try { run(`git fetch ${remote} ${branch} --quiet`, cwd); } catch {}
|
|
106
|
-
|
|
107
|
-
const content = capture(`git show ${remote}/${branch}:inferno/contract.json`, cwd);
|
|
108
|
-
if (!content) return null;
|
|
109
|
-
try { return JSON.parse(content); } catch { return null; }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function readLastSyncBase(infernoDir) {
|
|
113
|
-
const basePath = path.join(infernoDir, ".team-sync-base.json");
|
|
114
|
-
if (!fs.existsSync(basePath)) return null;
|
|
115
|
-
try { return JSON.parse(fs.readFileSync(basePath, "utf8")); } catch { return null; }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function writeLastSyncBase(infernoDir, contract) {
|
|
119
|
-
const basePath = path.join(infernoDir, ".team-sync-base.json");
|
|
120
|
-
fs.writeFileSync(basePath, JSON.stringify(contract, null, 2), "utf8");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── sub-commands ──────────────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
function initSharedBranch(cwd, remote, branch, infernoDir, asJson) {
|
|
126
|
-
if (!hasRemote(remote, cwd)) {
|
|
127
|
-
const msg = `Remote "${remote}" not found. Add it first: git remote add ${remote} <url>`;
|
|
128
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
129
|
-
warn(msg); process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (branchExistsRemote(remote, branch, cwd)) {
|
|
133
|
-
if (asJson) { console.log(JSON.stringify({ ok: true, action: "already_exists", branch })); }
|
|
134
|
-
else { ok(`Shared branch ${bold(branch)} already exists on ${remote}`); }
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Create orphan branch with just the contract
|
|
139
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
140
|
-
if (!fs.existsSync(contractPath)) {
|
|
141
|
-
const msg = "inferno/contract.json not found — run: infernoflow init";
|
|
142
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
143
|
-
warn(msg); process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Use a temp worktree approach: push contract.json to the branch
|
|
147
|
-
const tmpDir = path.join(infernoDir, ".team-sync-tmp");
|
|
148
|
-
try {
|
|
149
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
150
|
-
const contractContent = fs.readFileSync(contractPath, "utf8");
|
|
151
|
-
fs.writeFileSync(path.join(tmpDir, "contract.json"), contractContent);
|
|
152
|
-
|
|
153
|
-
// Create an empty tree commit on the shared branch
|
|
154
|
-
run(`git checkout --orphan ${branch}`, cwd);
|
|
155
|
-
run(`git rm -rf . --quiet 2>/dev/null || true`, cwd);
|
|
156
|
-
run(`git checkout ${currentBranch(cwd)} -- inferno/contract.json`, cwd);
|
|
157
|
-
run(`git add inferno/contract.json`, cwd);
|
|
158
|
-
run(`git commit -m "infernoflow: initialize shared contract branch"`, cwd);
|
|
159
|
-
run(`git push ${remote} ${branch}`, cwd);
|
|
160
|
-
run(`git checkout -`, cwd); // back to previous branch
|
|
161
|
-
} catch (err) {
|
|
162
|
-
// Restore original branch on failure
|
|
163
|
-
try { run(`git checkout -`, cwd); } catch {}
|
|
164
|
-
const msg = `Failed to create shared branch: ${err.message}`;
|
|
165
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
166
|
-
warn(msg); process.exit(1);
|
|
167
|
-
} finally {
|
|
168
|
-
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (asJson) { console.log(JSON.stringify({ ok: true, action: "created", branch, remote })); }
|
|
172
|
-
else { done(`Shared branch ${bold(branch)} created on ${bold(remote)}`); }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function pushToShared(cwd, remote, branch, infernoDir, asJson, force) {
|
|
176
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
177
|
-
if (!fs.existsSync(contractPath)) {
|
|
178
|
-
const msg = "inferno/contract.json not found";
|
|
179
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
180
|
-
warn(msg); process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const localContract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
|
|
184
|
-
const user = currentUser(cwd);
|
|
185
|
-
|
|
186
|
-
// Stamp the push metadata
|
|
187
|
-
localContract._teamSync = {
|
|
188
|
-
pushedBy: user,
|
|
189
|
-
pushedAt: new Date().toISOString(),
|
|
190
|
-
fromBranch: currentBranch(cwd),
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// Write updated contract
|
|
194
|
-
fs.writeFileSync(contractPath, JSON.stringify(localContract, null, 2), "utf8");
|
|
195
|
-
|
|
196
|
-
// Commit + push to shared branch
|
|
197
|
-
try {
|
|
198
|
-
run(`git fetch ${remote} ${branch} --quiet`, cwd);
|
|
199
|
-
// Use a temporary stash-push approach: push just the contract file
|
|
200
|
-
capture(`git stash --quiet`, cwd);
|
|
201
|
-
try {
|
|
202
|
-
run(`git checkout ${remote}/${branch} -- inferno/contract.json 2>/dev/null || git checkout ${remote}/${branch} inferno/contract.json`, cwd);
|
|
203
|
-
} catch {}
|
|
204
|
-
capture(`git stash pop --quiet`, cwd);
|
|
205
|
-
|
|
206
|
-
// Write the updated content
|
|
207
|
-
fs.writeFileSync(contractPath, JSON.stringify(localContract, null, 2), "utf8");
|
|
208
|
-
run(`git add inferno/contract.json`, cwd);
|
|
209
|
-
run(`git commit -m "infernoflow team-sync: push by ${user}"`, cwd);
|
|
210
|
-
run(`git push ${remote} HEAD:${branch}`, cwd);
|
|
211
|
-
|
|
212
|
-
// Save base snapshot
|
|
213
|
-
writeLastSyncBase(infernoDir, localContract);
|
|
214
|
-
|
|
215
|
-
if (asJson) { console.log(JSON.stringify({ ok: true, action: "pushed", remote, branch, user })); }
|
|
216
|
-
else { done(`Contract pushed to ${bold(remote + "/" + branch)} by ${bold(user)}`); }
|
|
217
|
-
} catch (err) {
|
|
218
|
-
const msg = `Push failed: ${err.message}`;
|
|
219
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
220
|
-
warn(msg);
|
|
221
|
-
info(`Try: git push ${remote} HEAD:${branch} --force (use --force flag)`);
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function pullFromShared(cwd, remote, branch, infernoDir, asJson, force) {
|
|
227
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
228
|
-
|
|
229
|
-
// Fetch remote contract
|
|
230
|
-
const sharedContract = readContractFromBranch(remote, branch, cwd);
|
|
231
|
-
if (!sharedContract) {
|
|
232
|
-
const msg = `Could not read contract from ${remote}/${branch}. Run: infernoflow team-sync init`;
|
|
233
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
234
|
-
warn(msg); process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const localContract = fs.existsSync(contractPath)
|
|
238
|
-
? JSON.parse(fs.readFileSync(contractPath, "utf8")) : { capabilities: [] };
|
|
239
|
-
|
|
240
|
-
const baseContract = readLastSyncBase(infernoDir) || { capabilities: [] };
|
|
241
|
-
|
|
242
|
-
const localCaps = parseCaps(JSON.stringify(localContract));
|
|
243
|
-
const sharedCaps = parseCaps(JSON.stringify(sharedContract));
|
|
244
|
-
const baseCaps = parseCaps(JSON.stringify(baseContract));
|
|
245
|
-
|
|
246
|
-
const { conflicts, localOnly, sharedOnly } = detectConflicts(localCaps, sharedCaps, baseCaps);
|
|
247
|
-
|
|
248
|
-
if (conflicts.length > 0 && !force) {
|
|
249
|
-
if (asJson) {
|
|
250
|
-
console.log(JSON.stringify({ ok: false, error: "conflicts_detected", conflicts, hint: "Use --force to overwrite with remote version" }));
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
warn(`${conflicts.length} capability conflict${conflicts.length !== 1 ? "s" : ""} detected:\n`);
|
|
254
|
-
for (const c of conflicts) {
|
|
255
|
-
console.log(` ${red("✗")} ${bold(c.id)}`);
|
|
256
|
-
console.log(` local: ${gray(c.local?.title || "(removed)")}`);
|
|
257
|
-
console.log(` shared: ${gray(c.shared?.title || "(removed)")}`);
|
|
258
|
-
}
|
|
259
|
-
console.log();
|
|
260
|
-
warn("Resolve conflicts manually or use --force to take the shared version");
|
|
261
|
-
process.exit(1);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Merge: take shared as base, apply localOnly additions
|
|
265
|
-
const merged = { ...sharedContract };
|
|
266
|
-
const mergedCaps = [...sharedCaps];
|
|
267
|
-
for (const cap of localOnly) mergedCaps.push(cap);
|
|
268
|
-
merged.capabilities = mergedCaps;
|
|
269
|
-
delete merged._teamSync;
|
|
270
|
-
|
|
271
|
-
fs.writeFileSync(contractPath, JSON.stringify(merged, null, 2), "utf8");
|
|
272
|
-
writeLastSyncBase(infernoDir, merged);
|
|
273
|
-
|
|
274
|
-
if (asJson) {
|
|
275
|
-
console.log(JSON.stringify({
|
|
276
|
-
ok: true, action: "pulled", remote, branch,
|
|
277
|
-
conflicts: conflicts.length,
|
|
278
|
-
localOnly: localOnly.length,
|
|
279
|
-
sharedOnly: sharedOnly.length,
|
|
280
|
-
}));
|
|
281
|
-
} else {
|
|
282
|
-
console.log();
|
|
283
|
-
ok("Contract updated from shared branch");
|
|
284
|
-
if (conflicts.length > 0) warn(`${conflicts.length} conflict(s) resolved with --force (shared version wins)`);
|
|
285
|
-
if (localOnly.length > 0) ok(`${localOnly.length} local capability(-ies) preserved`);
|
|
286
|
-
if (sharedOnly.length > 0) ok(`${sharedOnly.length} new capability(-ies) pulled from shared`);
|
|
287
|
-
if (conflicts.length === 0 && localOnly.length === 0 && sharedOnly.length === 0) {
|
|
288
|
-
info("Already in sync — no changes");
|
|
289
|
-
}
|
|
290
|
-
console.log();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function showStatus(cwd, remote, branch, infernoDir, asJson) {
|
|
295
|
-
const contractPath = path.join(infernoDir, "contract.json");
|
|
296
|
-
|
|
297
|
-
try { run(`git fetch ${remote} ${branch} --quiet`, cwd); } catch {}
|
|
298
|
-
|
|
299
|
-
const sharedContract = readContractFromBranch(remote, branch, cwd);
|
|
300
|
-
if (!sharedContract) {
|
|
301
|
-
const msg = `Shared branch ${remote}/${branch} not found. Run: infernoflow team-sync init`;
|
|
302
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
303
|
-
warn(msg); process.exit(1);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const localContract = fs.existsSync(contractPath)
|
|
307
|
-
? JSON.parse(fs.readFileSync(contractPath, "utf8")) : { capabilities: [] };
|
|
308
|
-
|
|
309
|
-
const localCaps = parseCaps(JSON.stringify(localContract));
|
|
310
|
-
const sharedCaps = parseCaps(JSON.stringify(sharedContract));
|
|
311
|
-
|
|
312
|
-
const localMap = capsToMap(localCaps);
|
|
313
|
-
const sharedMap = capsToMap(sharedCaps);
|
|
314
|
-
|
|
315
|
-
const onlyLocal = localCaps.filter(c => !sharedMap.has(c.id));
|
|
316
|
-
const onlyShared = sharedCaps.filter(c => !localMap.has(c.id));
|
|
317
|
-
const inSync = onlyLocal.length === 0 && onlyShared.length === 0;
|
|
318
|
-
const pushedBy = sharedContract._teamSync?.pushedBy || "unknown";
|
|
319
|
-
const pushedAt = sharedContract._teamSync?.pushedAt || "unknown";
|
|
320
|
-
|
|
321
|
-
if (asJson) {
|
|
322
|
-
console.log(JSON.stringify({
|
|
323
|
-
ok: true, inSync,
|
|
324
|
-
local: localCaps.length, shared: sharedCaps.length,
|
|
325
|
-
onlyLocal: onlyLocal.map(c => c.id),
|
|
326
|
-
onlyShared: onlyShared.map(c => c.id),
|
|
327
|
-
lastPush: { by: pushedBy, at: pushedAt },
|
|
328
|
-
}));
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
console.log();
|
|
333
|
-
console.log(` Shared branch ${bold(cyan(remote + "/" + branch))}`);
|
|
334
|
-
console.log(` Last push ${bold(pushedBy)} ${gray(pushedAt.slice(0, 19).replace("T", " "))}`);
|
|
335
|
-
console.log();
|
|
336
|
-
|
|
337
|
-
if (inSync) {
|
|
338
|
-
ok("Local and shared contracts are in sync");
|
|
339
|
-
} else {
|
|
340
|
-
if (onlyLocal.length) {
|
|
341
|
-
console.log(` ${yellow("→")} ${bold(onlyLocal.length)} local capability(-ies) not yet pushed:`);
|
|
342
|
-
for (const c of onlyLocal) console.log(` ${yellow("+")} ${c.id} ${gray(c.title)}`);
|
|
343
|
-
}
|
|
344
|
-
if (onlyShared.length) {
|
|
345
|
-
console.log(` ${cyan("←")} ${bold(onlyShared.length)} shared capability(-ies) not yet pulled:`);
|
|
346
|
-
for (const c of onlyShared) console.log(` ${cyan("+")} ${c.id} ${gray(c.title)}`);
|
|
347
|
-
}
|
|
348
|
-
console.log();
|
|
349
|
-
if (onlyLocal.length) info(`Run ${cyan("infernoflow team-sync push")} to share your changes`);
|
|
350
|
-
if (onlyShared.length) info(`Run ${cyan("infernoflow team-sync pull")} to get team changes`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
console.log();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ── main ──────────────────────────────────────────────────────────────────────
|
|
357
|
-
|
|
358
|
-
export async function teamSyncCommand(rawArgs) {
|
|
359
|
-
const args = rawArgs.slice(1);
|
|
360
|
-
const asJson = args.includes("--json");
|
|
361
|
-
const force = args.includes("--force");
|
|
362
|
-
|
|
363
|
-
const branchIdx = args.indexOf("--branch");
|
|
364
|
-
const remoteIdx = args.indexOf("--remote");
|
|
365
|
-
const branch = branchIdx !== -1 ? args[branchIdx + 1] : "inferno-contracts";
|
|
366
|
-
const remote = remoteIdx !== -1 ? args[remoteIdx + 1] : "origin";
|
|
367
|
-
|
|
368
|
-
const sub = args.find(a => !a.startsWith("-")) || "status";
|
|
369
|
-
|
|
370
|
-
const cwd = process.cwd();
|
|
371
|
-
const infernoDir = path.join(cwd, "inferno");
|
|
372
|
-
|
|
373
|
-
if (!asJson) header("infernoflow team-sync");
|
|
374
|
-
|
|
375
|
-
if (!fs.existsSync(infernoDir)) {
|
|
376
|
-
const msg = "inferno/ not found — run: infernoflow init";
|
|
377
|
-
if (asJson) { console.log(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
|
|
378
|
-
warn(msg); process.exit(1);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
switch (sub) {
|
|
382
|
-
case "init": initSharedBranch(cwd, remote, branch, infernoDir, asJson); break;
|
|
383
|
-
case "push": pushToShared(cwd, remote, branch, infernoDir, asJson, force); break;
|
|
384
|
-
case "pull": pullFromShared(cwd, remote, branch, infernoDir, asJson, force); break;
|
|
385
|
-
case "status":
|
|
386
|
-
default: showStatus(cwd, remote, branch, infernoDir, asJson); break;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
1
|
+
import*as g from"node:fs";import*as N from"node:path";import{execSync as A}from"node:child_process";import{header as L,ok as j,warn as S,info as F,done as B,bold as m,cyan as b,gray as C,red as _,yellow as P}from"../ui/output.mjs";function x(t,n){try{return A(t,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}catch{return null}}function p(t,n){A(t,{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function R(t){return x("git rev-parse --abbrev-ref HEAD",t)||"HEAD"}function E(t){return x("git config user.name",t)||x("git config user.email",t)||"unknown"}function H(t,n){return!!x(`git remote get-url ${t}`,n)}function I(t,n,e){return!!x(`git ls-remote --heads ${t} refs/heads/${n}`,e)}function v(t){if(!t)return[];try{return(JSON.parse(t).capabilities||[]).map(s=>typeof s=="string"?{id:s,title:s}:s)}catch{return[]}}function w(t){return new Map(t.map(n=>[n.id,n]))}function D(t,n,e){const s=w(t),c=w(n),d=w(e),i=[],o=[],l=[],f=new Set([...s.keys(),...c.keys(),...d.keys()]);for(const u of f){const h=s.get(u),y=c.get(u),r=d.get(u),$=JSON.stringify(h)!==JSON.stringify(r),O=JSON.stringify(y)!==JSON.stringify(r);$&&O&&JSON.stringify(h)!==JSON.stringify(y)?i.push({id:u,local:h,shared:y,base:r}):h&&!y&&!r?o.push(h):!h&&y&&!r&&l.push(y)}return{conflicts:i,localOnly:o,sharedOnly:l}}function M(t,n,e){try{p(`git fetch ${t} ${n} --quiet`,e)}catch{}const s=x(`git show ${t}/${n}:inferno/contract.json`,e);if(!s)return null;try{return JSON.parse(s)}catch{return null}}function T(t){const n=N.join(t,".team-sync-base.json");if(!g.existsSync(n))return null;try{return JSON.parse(g.readFileSync(n,"utf8"))}catch{return null}}function q(t,n){const e=N.join(t,".team-sync-base.json");g.writeFileSync(e,JSON.stringify(n,null,2),"utf8")}function U(t,n,e,s,c){if(!H(n,t)){const o=`Remote "${n}" not found. Add it first: git remote add ${n} <url>`;c&&(console.log(JSON.stringify({ok:!1,error:o})),process.exit(1)),S(o),process.exit(1)}if(I(n,e,t)){c?console.log(JSON.stringify({ok:!0,action:"already_exists",branch:e})):j(`Shared branch ${m(e)} already exists on ${n}`);return}const d=N.join(s,"contract.json");if(!g.existsSync(d)){const o="inferno/contract.json not found \u2014 run: infernoflow init";c&&(console.log(JSON.stringify({ok:!1,error:o})),process.exit(1)),S(o),process.exit(1)}const i=N.join(s,".team-sync-tmp");try{g.mkdirSync(i,{recursive:!0});const o=g.readFileSync(d,"utf8");g.writeFileSync(N.join(i,"contract.json"),o),p(`git checkout --orphan ${e}`,t),p("git rm -rf . --quiet 2>/dev/null || true",t),p(`git checkout ${R(t)} -- inferno/contract.json`,t),p("git add inferno/contract.json",t),p('git commit -m "infernoflow: initialize shared contract branch"',t),p(`git push ${n} ${e}`,t),p("git checkout -",t)}catch(o){try{p("git checkout -",t)}catch{}const l=`Failed to create shared branch: ${o.message}`;c&&(console.log(JSON.stringify({ok:!1,error:l})),process.exit(1)),S(l),process.exit(1)}finally{try{g.rmSync(i,{recursive:!0})}catch{}}c?console.log(JSON.stringify({ok:!0,action:"created",branch:e,remote:n})):B(`Shared branch ${m(e)} created on ${m(n)}`)}function z(t,n,e,s,c,d){const i=N.join(s,"contract.json");if(!g.existsSync(i)){const f="inferno/contract.json not found";c&&(console.log(JSON.stringify({ok:!1,error:f})),process.exit(1)),S(f),process.exit(1)}const o=JSON.parse(g.readFileSync(i,"utf8")),l=E(t);o._teamSync={pushedBy:l,pushedAt:new Date().toISOString(),fromBranch:R(t)},g.writeFileSync(i,JSON.stringify(o,null,2),"utf8");try{p(`git fetch ${n} ${e} --quiet`,t),x("git stash --quiet",t);try{p(`git checkout ${n}/${e} -- inferno/contract.json 2>/dev/null || git checkout ${n}/${e} inferno/contract.json`,t)}catch{}x("git stash pop --quiet",t),g.writeFileSync(i,JSON.stringify(o,null,2),"utf8"),p("git add inferno/contract.json",t),p(`git commit -m "infernoflow team-sync: push by ${l}"`,t),p(`git push ${n} HEAD:${e}`,t),q(s,o),c?console.log(JSON.stringify({ok:!0,action:"pushed",remote:n,branch:e,user:l})):B(`Contract pushed to ${m(n+"/"+e)} by ${m(l)}`)}catch(f){const u=`Push failed: ${f.message}`;c&&(console.log(JSON.stringify({ok:!1,error:u})),process.exit(1)),S(u),F(`Try: git push ${n} HEAD:${e} --force (use --force flag)`),process.exit(1)}}function W(t,n,e,s,c,d){const i=N.join(s,"contract.json"),o=M(n,e,t);if(!o){const k=`Could not read contract from ${n}/${e}. Run: infernoflow team-sync init`;c&&(console.log(JSON.stringify({ok:!1,error:k})),process.exit(1)),S(k),process.exit(1)}const l=g.existsSync(i)?JSON.parse(g.readFileSync(i,"utf8")):{capabilities:[]},f=T(s)||{capabilities:[]},u=v(JSON.stringify(l)),h=v(JSON.stringify(o)),y=v(JSON.stringify(f)),{conflicts:r,localOnly:$,sharedOnly:O}=D(u,h,y);if(r.length>0&&!d){c&&(console.log(JSON.stringify({ok:!1,error:"conflicts_detected",conflicts:r,hint:"Use --force to overwrite with remote version"})),process.exit(1)),S(`${r.length} capability conflict${r.length!==1?"s":""} detected:
|
|
2
|
+
`);for(const k of r)console.log(` ${_("\u2717")} ${m(k.id)}`),console.log(` local: ${C(k.local?.title||"(removed)")}`),console.log(` shared: ${C(k.shared?.title||"(removed)")}`);console.log(),S("Resolve conflicts manually or use --force to take the shared version"),process.exit(1)}const J={...o},a=[...h];for(const k of $)a.push(k);J.capabilities=a,delete J._teamSync,g.writeFileSync(i,JSON.stringify(J,null,2),"utf8"),q(s,J),c?console.log(JSON.stringify({ok:!0,action:"pulled",remote:n,branch:e,conflicts:r.length,localOnly:$.length,sharedOnly:O.length})):(console.log(),j("Contract updated from shared branch"),r.length>0&&S(`${r.length} conflict(s) resolved with --force (shared version wins)`),$.length>0&&j(`${$.length} local capability(-ies) preserved`),O.length>0&&j(`${O.length} new capability(-ies) pulled from shared`),r.length===0&&$.length===0&&O.length===0&&F("Already in sync \u2014 no changes"),console.log())}function G(t,n,e,s,c){const d=N.join(s,"contract.json");try{p(`git fetch ${n} ${e} --quiet`,t)}catch{}const i=M(n,e,t);if(!i){const a=`Shared branch ${n}/${e} not found. Run: infernoflow team-sync init`;c&&(console.log(JSON.stringify({ok:!1,error:a})),process.exit(1)),S(a),process.exit(1)}const o=g.existsSync(d)?JSON.parse(g.readFileSync(d,"utf8")):{capabilities:[]},l=v(JSON.stringify(o)),f=v(JSON.stringify(i)),u=w(l),h=w(f),y=l.filter(a=>!h.has(a.id)),r=f.filter(a=>!u.has(a.id)),$=y.length===0&&r.length===0,O=i._teamSync?.pushedBy||"unknown",J=i._teamSync?.pushedAt||"unknown";if(c){console.log(JSON.stringify({ok:!0,inSync:$,local:l.length,shared:f.length,onlyLocal:y.map(a=>a.id),onlyShared:r.map(a=>a.id),lastPush:{by:O,at:J}}));return}if(console.log(),console.log(` Shared branch ${m(b(n+"/"+e))}`),console.log(` Last push ${m(O)} ${C(J.slice(0,19).replace("T"," "))}`),console.log(),$)j("Local and shared contracts are in sync");else{if(y.length){console.log(` ${P("\u2192")} ${m(y.length)} local capability(-ies) not yet pushed:`);for(const a of y)console.log(` ${P("+")} ${a.id} ${C(a.title)}`)}if(r.length){console.log(` ${b("\u2190")} ${m(r.length)} shared capability(-ies) not yet pulled:`);for(const a of r)console.log(` ${b("+")} ${a.id} ${C(a.title)}`)}console.log(),y.length&&F(`Run ${b("infernoflow team-sync push")} to share your changes`),r.length&&F(`Run ${b("infernoflow team-sync pull")} to get team changes`)}console.log()}async function X(t){const n=t.slice(1),e=n.includes("--json"),s=n.includes("--force"),c=n.indexOf("--branch"),d=n.indexOf("--remote"),i=c!==-1?n[c+1]:"inferno-contracts",o=d!==-1?n[d+1]:"origin",l=n.find(h=>!h.startsWith("-"))||"status",f=process.cwd(),u=N.join(f,"inferno");if(e||L("infernoflow team-sync"),!g.existsSync(u)){const h="inferno/ not found \u2014 run: infernoflow init";e&&(console.log(JSON.stringify({ok:!1,error:h})),process.exit(1)),S(h),process.exit(1)}switch(l){case"init":U(f,o,i,u,e);break;case"push":z(f,o,i,u,e,s);break;case"pull":W(f,o,i,u,e,s);break;default:G(f,o,i,u,e);break}}export{X as teamSyncCommand};
|