unoverse 0.1.163 → 0.1.165
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/lib/publish.mjs +4 -2
- package/package.json +1 -1
- package/vendor/base/items/publish.js +49 -2
package/lib/publish.mjs
CHANGED
|
@@ -140,7 +140,7 @@ export async function publish(args) {
|
|
|
140
140
|
// ── 3. the plan: what would this publish actually do ────────────────────────
|
|
141
141
|
let plan;
|
|
142
142
|
try {
|
|
143
|
-
plan = await planPublish(items, universe, token);
|
|
143
|
+
plan = await planPublish(items, universe, token, project);
|
|
144
144
|
} catch (e) {
|
|
145
145
|
die(e.message);
|
|
146
146
|
}
|
|
@@ -152,8 +152,10 @@ export async function publish(args) {
|
|
|
152
152
|
for (const i of plan.update) console.log(` ${c.yellow}~${c.off} ${label(i)} ${c.dim}(changed)${c.off}${pending(i)}`);
|
|
153
153
|
if (plan.unchanged.length) console.log(` ${c.dim}= ${plan.unchanged.length} unchanged${c.off}`);
|
|
154
154
|
for (const r of plan.refused) console.log(` ${c.red}✗ ${label(r)}${c.off} ${c.dim}${r.why}${c.off}`);
|
|
155
|
+
// Deploy is a SYNC: what left the workspace leaves the universe, said before it happens.
|
|
156
|
+
for (const r of plan.remove ?? []) console.log(` ${c.red}-${c.off} ${label(r)} ${c.dim}(no longer in this workspace — will be removed)${c.off}`);
|
|
155
157
|
|
|
156
|
-
const toSend = plan.create.length + plan.update.length;
|
|
158
|
+
const toSend = plan.create.length + plan.update.length + (plan.remove?.length ?? 0);
|
|
157
159
|
if (!toSend) {
|
|
158
160
|
console.log(`\n ${c.green}✓${c.off} nothing to do, ${project} is up to date\n`);
|
|
159
161
|
process.exit(plan.refused.length ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -35,8 +35,41 @@ export async function lintForPublish(designRoot, project) {
|
|
|
35
35
|
* owned by another publisher is refused, and finding that out before a publish beats
|
|
36
36
|
* finding it out halfway through one.
|
|
37
37
|
*/
|
|
38
|
-
export async function planPublish(items, universe, token,
|
|
39
|
-
|
|
38
|
+
export async function planPublish(items, universe, token,
|
|
39
|
+
/** The project being deployed; names the org whose universe rows the sync reconciles. */
|
|
40
|
+
project = "", fetchImpl = fetch) {
|
|
41
|
+
const plan = { create: [], update: [], unchanged: [], refused: [], remove: [] };
|
|
42
|
+
/**
|
|
43
|
+
* DEPLOY IS A SYNC (ruled 2026-08-21): what the workspace no longer holds, the
|
|
44
|
+
* universe should not keep serving under this project's name. The universe lists its
|
|
45
|
+
* rows for the project; anything absent from the collected items joins the plan as a
|
|
46
|
+
* removal, shown and confirmed like everything else.
|
|
47
|
+
*
|
|
48
|
+
* ONE GUARD, against the catastrophic misread: a workspace that holds NO items for
|
|
49
|
+
* the project (a fresh clone missing its files, the wrong folder entirely) does not
|
|
50
|
+
* get to empty the universe. Removals are proposed only when the workspace holds at
|
|
51
|
+
* least one item — deleting the last one is done by name, deliberately.
|
|
52
|
+
*/
|
|
53
|
+
if (project && items.length > 0) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetchImpl(`${universe}/publish`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
58
|
+
body: JSON.stringify({ op: "list", project }),
|
|
59
|
+
});
|
|
60
|
+
if (res.ok) {
|
|
61
|
+
const doc = (await res.json().catch(() => null));
|
|
62
|
+
const have = new Set(items.map((i) => `${i.kind}/${i.name}`));
|
|
63
|
+
for (const r of doc?.items ?? []) {
|
|
64
|
+
if (!have.has(`${r.kind}/${r.name}`))
|
|
65
|
+
plan.remove.push(r);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// An older universe answers this op with an error: no removals proposed, the
|
|
69
|
+
// additive behavior every deploy had before. Never a failure.
|
|
70
|
+
}
|
|
71
|
+
catch { /* unreachable universe fails later, on the real sends */ }
|
|
72
|
+
}
|
|
40
73
|
for (const item of items) {
|
|
41
74
|
// ASK THE GATE, not the item store. `/api/items` is deliberately shut (§9.10), so a
|
|
42
75
|
// publisher cannot read rows directly; and asking the publish route means the plan is
|
|
@@ -88,6 +121,20 @@ export async function sendPublish(plan, universe, token, fetchImpl = fetch) {
|
|
|
88
121
|
else
|
|
89
122
|
failed.push({ ...item, why: body.error ?? `HTTP ${res.status}` });
|
|
90
123
|
}
|
|
124
|
+
// Removals LAST, after the writes landed: a sync that dies mid-run has then added
|
|
125
|
+
// before it has taken away, which is the recoverable order.
|
|
126
|
+
for (const r of plan.remove ?? []) {
|
|
127
|
+
const res = await fetchImpl(`${universe}/publish`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
130
|
+
body: JSON.stringify({ op: "remove", kind: r.kind, name: r.name }),
|
|
131
|
+
});
|
|
132
|
+
const body = (await res.json().catch(() => ({})));
|
|
133
|
+
if (res.ok)
|
|
134
|
+
sent.push({ ...r, mode: "removed" });
|
|
135
|
+
else
|
|
136
|
+
failed.push({ ...r, why: body.error ?? `HTTP ${res.status}` });
|
|
137
|
+
}
|
|
91
138
|
return { sent, failed };
|
|
92
139
|
}
|
|
93
140
|
export { collectProject };
|