@mutmutco/cli 2.25.1 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.cjs +74 -24
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -10587,6 +10587,32 @@ async function planPush(deps, slug, opts = {}) {
|
|
|
10587
10587
|
deps.log(`queued ${slug} for background push \u2014 expected, not a failure (\`mmi-cli northstar status\` to check; \`mmi-cli northstar sync\` or --wait for durable confirmation)`);
|
|
10588
10588
|
return true;
|
|
10589
10589
|
}
|
|
10590
|
+
function buildPutBody(project2, slug, content, opts = {}) {
|
|
10591
|
+
const body = { project: project2, slug, content };
|
|
10592
|
+
const frontmatterMeta = extractPlanMeta(content);
|
|
10593
|
+
if (Object.keys(frontmatterMeta).length) body.meta = frontmatterMeta;
|
|
10594
|
+
if (opts.force) body.force = true;
|
|
10595
|
+
else if (opts.baseEtag) body.baseEtag = opts.baseEtag;
|
|
10596
|
+
return body;
|
|
10597
|
+
}
|
|
10598
|
+
async function reconcilePushConflict(deps, project2, slug, content, baseHash) {
|
|
10599
|
+
const qs = new URLSearchParams({ project: project2, slug }).toString();
|
|
10600
|
+
const pull = await deps.fetch(`${deps.apiUrl}/plan/get?${qs}`, { method: "GET", headers: await deps.headers(), signal: AbortSignal.timeout(TIMEOUT_MS) }).catch(() => null);
|
|
10601
|
+
const doc = pull?.ok ? await pull.json() : null;
|
|
10602
|
+
if (!doc) return null;
|
|
10603
|
+
const remote = normalizeEol(doc.content ?? "");
|
|
10604
|
+
if (remote === content) return { etag: doc.etag };
|
|
10605
|
+
if (baseHash && hashContent(remote) === baseHash) {
|
|
10606
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
10607
|
+
method: "POST",
|
|
10608
|
+
headers: await deps.headers({ "content-type": "application/json" }),
|
|
10609
|
+
body: JSON.stringify(buildPutBody(project2, slug, content, { baseEtag: doc.etag })),
|
|
10610
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
10611
|
+
}).catch(() => null);
|
|
10612
|
+
if (res?.ok) return await res.json();
|
|
10613
|
+
}
|
|
10614
|
+
return null;
|
|
10615
|
+
}
|
|
10590
10616
|
async function planPushNow(deps, slug, opts = {}) {
|
|
10591
10617
|
const raw = deps.readLocal(slug);
|
|
10592
10618
|
if (raw == null) {
|
|
@@ -10597,25 +10623,21 @@ async function planPushNow(deps, slug, opts = {}) {
|
|
|
10597
10623
|
const project2 = opts.project ?? await deps.project();
|
|
10598
10624
|
const meta = parseMeta(deps.readMetaRaw());
|
|
10599
10625
|
const entry = meta[metaKey(project2, slug)];
|
|
10600
|
-
const body = { project: project2, slug, content };
|
|
10601
|
-
const frontmatterMeta = extractPlanMeta(content);
|
|
10602
|
-
if (Object.keys(frontmatterMeta).length) body.meta = frontmatterMeta;
|
|
10603
|
-
if (opts.force) body.force = true;
|
|
10604
|
-
else if (entry?.etag) body.baseEtag = entry.etag;
|
|
10605
10626
|
const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
10606
10627
|
method: "POST",
|
|
10607
10628
|
headers: await deps.headers({ "content-type": "application/json" }),
|
|
10608
|
-
body: JSON.stringify(
|
|
10629
|
+
body: JSON.stringify(buildPutBody(project2, slug, content, { force: opts.force, baseEtag: entry?.etag })),
|
|
10609
10630
|
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
10610
10631
|
});
|
|
10611
|
-
if (res.ok) {
|
|
10612
|
-
const
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10632
|
+
if (res.ok || res.status === 409) {
|
|
10633
|
+
const adopted = res.ok ? await res.json() : await reconcilePushConflict(deps, project2, slug, content, entry?.hash);
|
|
10634
|
+
if (adopted) {
|
|
10635
|
+
meta[metaKey(project2, slug)] = { etag: adopted.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
10636
|
+
deps.writeMetaRaw(serializeMeta(meta));
|
|
10637
|
+
dropQueued(deps, project2, slug);
|
|
10638
|
+
deps.log(`pushed ${slug}`);
|
|
10639
|
+
return true;
|
|
10640
|
+
}
|
|
10619
10641
|
deps.err(staleHint(slug));
|
|
10620
10642
|
return false;
|
|
10621
10643
|
} else {
|
|
@@ -10766,11 +10788,7 @@ async function planSync(deps, opts = {}) {
|
|
|
10766
10788
|
const content = normalizeEol(raw);
|
|
10767
10789
|
const meta = parseMeta(deps.readMetaRaw());
|
|
10768
10790
|
const metaEntry = meta[metaKey(entry.project, entry.slug)];
|
|
10769
|
-
const body =
|
|
10770
|
-
const frontmatterMeta = extractPlanMeta(content);
|
|
10771
|
-
if (Object.keys(frontmatterMeta).length) body.meta = frontmatterMeta;
|
|
10772
|
-
if (entry.force) body.force = true;
|
|
10773
|
-
else if (metaEntry?.etag) body.baseEtag = metaEntry.etag;
|
|
10791
|
+
const body = buildPutBody(entry.project, entry.slug, content, { force: entry.force, baseEtag: metaEntry?.etag });
|
|
10774
10792
|
let res;
|
|
10775
10793
|
try {
|
|
10776
10794
|
res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
|
|
@@ -10789,12 +10807,11 @@ async function planSync(deps, opts = {}) {
|
|
|
10789
10807
|
deps.writeMetaRaw(serializeMeta(meta));
|
|
10790
10808
|
if (!opts.quiet) deps.log(`pushed ${entry.slug}`);
|
|
10791
10809
|
} else if (res.status === 409) {
|
|
10792
|
-
const
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
if (doc && normalizeEol(doc.content ?? "") === content) {
|
|
10796
|
-
meta[metaKey(entry.project, entry.slug)] = { etag: doc.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
10810
|
+
const adopted = await reconcilePushConflict(deps, entry.project, entry.slug, content, metaEntry?.hash);
|
|
10811
|
+
if (adopted) {
|
|
10812
|
+
meta[metaKey(entry.project, entry.slug)] = { etag: adopted.etag, hash: hashContent(content), syncedAt: deps.now() };
|
|
10797
10813
|
deps.writeMetaRaw(serializeMeta(meta));
|
|
10814
|
+
if (!opts.quiet) deps.log(`pushed ${entry.slug}`);
|
|
10798
10815
|
} else {
|
|
10799
10816
|
kept.push({ ...entry, attempts: entry.attempts + 1, conflict: staleHint(entry.slug) });
|
|
10800
10817
|
}
|
|
@@ -10822,6 +10839,38 @@ async function planStatus(deps) {
|
|
|
10822
10839
|
deps.log(idx ? `plan index: ${idx.plans.length} plans, fetched ${idx.fetchedAt}` : "plan index: cold (first read will fetch)");
|
|
10823
10840
|
if (queue.some((e) => e.conflict || e.deadLettered)) process.exitCode = 1;
|
|
10824
10841
|
}
|
|
10842
|
+
async function planReconcile(deps) {
|
|
10843
|
+
const meta = parseMeta(deps.readMetaRaw());
|
|
10844
|
+
let refreshed = 0;
|
|
10845
|
+
let diverged = 0;
|
|
10846
|
+
let unreachable = 0;
|
|
10847
|
+
for (const [key, entry] of Object.entries(meta)) {
|
|
10848
|
+
const i = key.indexOf("/");
|
|
10849
|
+
if (i < 0) continue;
|
|
10850
|
+
const project2 = key.slice(0, i);
|
|
10851
|
+
const slug = key.slice(i + 1);
|
|
10852
|
+
const qs = new URLSearchParams({ project: project2, slug }).toString();
|
|
10853
|
+
const res = await deps.fetch(`${deps.apiUrl}/plan/get?${qs}`, { method: "GET", headers: await deps.headers(), signal: AbortSignal.timeout(TIMEOUT_MS) }).catch(() => null);
|
|
10854
|
+
if (!res || !res.ok) {
|
|
10855
|
+
unreachable++;
|
|
10856
|
+
continue;
|
|
10857
|
+
}
|
|
10858
|
+
const doc = await res.json();
|
|
10859
|
+
const remote = normalizeEol(doc.content ?? "");
|
|
10860
|
+
if (!entry.hash || hashContent(remote) !== entry.hash) {
|
|
10861
|
+
diverged++;
|
|
10862
|
+
continue;
|
|
10863
|
+
}
|
|
10864
|
+
if (doc.etag && doc.etag !== entry.etag) {
|
|
10865
|
+
meta[key] = { ...entry, etag: doc.etag, syncedAt: deps.now() };
|
|
10866
|
+
refreshed++;
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
if (refreshed) deps.writeMetaRaw(serializeMeta(meta));
|
|
10870
|
+
deps.log(
|
|
10871
|
+
`reconcile: ${refreshed} etag(s) refreshed` + (diverged ? `, ${diverged} diverged (pull or push them)` : "") + (unreachable ? `, ${unreachable} unreachable` : "")
|
|
10872
|
+
);
|
|
10873
|
+
}
|
|
10825
10874
|
async function planGraduate(deps, slug, opts = {}) {
|
|
10826
10875
|
if (!opts.orgVisible) {
|
|
10827
10876
|
deps.err("refusing to mark an org-visible graduation without --org-visible");
|
|
@@ -11913,6 +11962,7 @@ function registerNorthStarCommands(cmd) {
|
|
|
11913
11962
|
}));
|
|
11914
11963
|
cmd.command("sync").description("drain queued background pushes and refresh the local plan index").option("--quiet", "silent (background worker)").action((o) => withPlan(o.quiet ?? false, (d) => planSync(d, o)));
|
|
11915
11964
|
cmd.command("status").description("show pending/conflicted background pushes and the plan-cache age").action(() => withPlan(false, (d) => planStatus(d)));
|
|
11965
|
+
cmd.command("reconcile").description("refresh stale local etags from the server without --force (recovers from an object-store re-stamp)").action(() => withPlan(false, (d) => planReconcile(d)));
|
|
11916
11966
|
cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
|
|
11917
11967
|
(slug, o) => withPlan(false, async (d) => {
|
|
11918
11968
|
const ok = await planPull(d, slug, { project: o.project });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|