@rightkit/release 0.2.128 → 0.2.130

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.128",
3
+ "version": "0.2.130",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/rightkit-sync.mjs CHANGED
@@ -35,13 +35,71 @@ export function rightkitPublishables(root = rightkitRoot) {
35
35
  return { crates, packages };
36
36
  }
37
37
 
38
+ const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
39
+
38
40
  export function compareVersions(left, right) {
39
- const parse = (value) => String(value).split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
40
- const a = parse(left); const b = parse(right);
41
- for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] - b[i];
41
+ const a = String(left).match(SEMVER); const b = String(right).match(SEMVER);
42
+ if (!a || !b) throw new Error(`invalid semver: ${!a ? left : right}`);
43
+ const compare = (x, y) => x < y ? -1 : x > y ? 1 : 0;
44
+ for (let i = 1; i <= 3; i += 1) {
45
+ const order = compare(BigInt(a[i]), BigInt(b[i]));
46
+ if (order) return order;
47
+ }
48
+ if (!a[4] || !b[4]) return compare(Boolean(b[4]), Boolean(a[4]));
49
+ const ap = a[4].split("."); const bp = b[4].split(".");
50
+ for (let i = 0; i < Math.max(ap.length, bp.length); i += 1) {
51
+ if (ap[i] === undefined || bp[i] === undefined) return compare(ap.length, bp.length);
52
+ const an = /^\d+$/.test(ap[i]); const bn = /^\d+$/.test(bp[i]);
53
+ const order = an && bn ? compare(BigInt(ap[i]), BigInt(bp[i]))
54
+ : an !== bn ? (an ? -1 : 1) : compare(ap[i], bp[i]);
55
+ if (order) return order;
56
+ }
42
57
  return 0;
43
58
  }
44
59
 
60
+ const versionKeys = (registry) => {
61
+ if (!["npm", "cargo"].includes(registry)) throw new Error(`unknown registry: ${registry}; expected npm or cargo`);
62
+ return registry === "npm" ? ["stagedNpm", "legacyNpm"] : ["stagedCargo", "legacyCargo"];
63
+ };
64
+
65
+ // Use publication evidence captured before replacing registry[name], never staging alone.
66
+ function retainPublished(versions, { registry, name, version }, candidates, latest) {
67
+ const [, legacyKey] = versionKeys(registry);
68
+ const recorded = versions.registry?.[registry]?.[name];
69
+ const eligible = candidates.filter((previous) => previous && compareVersions(previous, version) < 0
70
+ && (previous === recorded || previous === latest));
71
+ if (!eligible.length) return;
72
+ versions[legacyKey] ??= {};
73
+ versions[legacyKey][name] = [...new Set([...(versions[legacyKey][name] ?? []), ...eligible])].sort(compareVersions);
74
+ }
75
+
76
+ /** Stage a rollout while preserving published pins, then refresh consumer copies. */
77
+ export async function stageVersion({ registry, name, version }, { file = VERSIONS_FILE, root = rightkitRoot, log = (line) => process.stdout.write(`${line}\n`), fetchImpl = fetch } = {}) {
78
+ const [stagedKey] = versionKeys(registry);
79
+ if (typeof name !== "string" || !name.trim()) throw new Error("stage requires a package or crate name");
80
+ if (typeof version !== "string" || !SEMVER.test(version)) throw new Error(`invalid semver: ${version}`);
81
+ const versions = JSON.parse(readFileSync(file, "utf8"));
82
+ const current = versions[stagedKey]?.[name] ?? versions[registry]?.[name] ?? null;
83
+ if (current && compareVersions(version, current) < 0) throw new Error(`refusing to stage ${name} ${version} below ${current}`);
84
+ const next = structuredClone(versions);
85
+ const recorded = next.registry?.[registry]?.[name];
86
+ let latest;
87
+ if (current && compareVersions(current, version) < 0 && current !== recorded) {
88
+ const [result] = await registryLatest({ crates: registry === "cargo" ? [name] : [], packages: registry === "npm" ? [name] : [] }, { fetchImpl });
89
+ latest = result.version;
90
+ }
91
+ retainPublished(next, { registry, name, version }, [current, recorded], latest);
92
+ next[stagedKey] ??= {};
93
+ next[stagedKey][name] = version;
94
+ next[stagedKey] = Object.fromEntries(Object.entries(next[stagedKey]).sort(([a], [b]) => a.localeCompare(b)));
95
+ const written = JSON.stringify(next) !== JSON.stringify(versions);
96
+ if (written) writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
97
+ const copies = syncConsumerCopies(root);
98
+ log(`${registry} ${name}: ${current ?? "unlisted"} -> ${version}`);
99
+ for (const copy of copies) log(`refreshed ${copy}`);
100
+ return { changes: current !== version ? [{ registry, name, from: current, to: version }] : [], copies, written };
101
+ }
102
+
45
103
  async function fetchJson(fetchImpl, url) {
46
104
  const response = await fetchImpl(url, { headers: { "user-agent": USER_AGENT, accept: "application/json" }, signal: AbortSignal.timeout(15_000) });
47
105
  if (response.status === 404) return null;
@@ -71,20 +129,19 @@ export function applyRegistryLatest(versions, latest) {
71
129
  const next = structuredClone(versions); const changes = []; let verified = 0;
72
130
  for (const { registry, name, version } of latest) {
73
131
  if (!version) continue;
132
+ const [stagedKey] = versionKeys(registry);
133
+ const current = next[stagedKey]?.[name] ?? next[registry]?.[name] ?? null;
134
+ const recorded = next.registry?.[registry]?.[name];
135
+ retainPublished(next, { registry, name, version }, [current, recorded], version);
136
+ // A registry can lag an unpublished staged rollout: its published pin still stays allowed.
137
+ if (current && compareVersions(version, current) < 0) {
138
+ retainPublished(next, { registry, name, version: current }, [recorded, version], version);
139
+ }
74
140
  // What the registry serves: the only versions adoption recommends (staged may still be unpublished).
75
141
  next.registry ??= { cargo: {}, npm: {} }; next.registry[registry] ??= {};
76
142
  if (next.registry[registry][name] !== version) { next.registry[registry][name] = version; verified += 1; }
77
- const stagedKey = registry === "cargo" ? "stagedCargo" : "stagedNpm";
78
- const legacyKey = registry === "cargo" ? "legacyCargo" : "legacyNpm";
79
- const current = next[stagedKey]?.[name] ?? next[registry]?.[name] ?? null;
80
143
  if (current && compareVersions(version, current) <= 0) continue;
81
144
  next[stagedKey] ??= {};
82
- const previous = next[stagedKey][name];
83
- if (previous && previous !== next[registry]?.[name]) {
84
- next[legacyKey] ??= {};
85
- const legacy = new Set(next[legacyKey][name] ?? []); legacy.add(previous);
86
- next[legacyKey][name] = [...legacy].sort(compareVersions);
87
- }
88
145
  next[stagedKey][name] = version;
89
146
  next[stagedKey] = Object.fromEntries(Object.entries(next[stagedKey]).sort(([a], [b]) => a.localeCompare(b)));
90
147
  changes.push({ registry, name, from: current, to: version });
@@ -96,24 +153,26 @@ export function applyRegistryLatest(versions, latest) {
96
153
  export async function syncRightkitVersions({ check = false, fetchImpl = fetch, file = VERSIONS_FILE, root = rightkitRoot, log = (line) => process.stdout.write(`${line}\n`) } = {}) {
97
154
  const versions = JSON.parse(readFileSync(file, "utf8"));
98
155
  const latest = await registryLatest(rightkitPublishables(root), { fetchImpl });
99
- const { versions: next, changes, verified } = applyRegistryLatest(versions, latest);
156
+ const { versions: next, changes } = applyRegistryLatest(versions, latest);
100
157
  for (const change of changes) log(`${change.registry} ${change.name}: ${change.from ?? "unlisted"} -> ${change.to}`);
101
158
  const unpublished = latest.filter((item) => !item.version).map((item) => `${item.registry}:${item.name}`);
102
159
  if (unpublished.length) log(`never published: ${unpublished.join(", ")}`);
103
160
  if (!changes.length) log("rightkit-versions.json matches the registries");
104
161
  if (check) return { changes, unpublished, copies: [], written: false };
105
- if (changes.length || verified) writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
162
+ const written = JSON.stringify(next) !== JSON.stringify(versions);
163
+ if (written) writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
106
164
  const copies = syncConsumerCopies(root);
107
165
  for (const copy of copies) log(`refreshed ${copy}`);
108
- return { changes, unpublished, copies, written: changes.length > 0 };
166
+ return { changes, unpublished, copies, written };
109
167
  }
110
168
 
111
169
  /** Record one just-published version (the registry index can lag the upload) and refresh the copies.
112
170
  * Called by right-release after a verified cargo or npm publish; never fails the publish itself. */
113
171
  export function recordPublished({ registry, name, version }, { file = VERSIONS_FILE, root = rightkitRoot, log = (line) => process.stdout.write(`${line}\n`) } = {}) {
114
172
  try {
115
- const { versions: next, changes, verified } = applyRegistryLatest(JSON.parse(readFileSync(file, "utf8")), [{ registry, name, version }]);
116
- if (changes.length || verified) writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
173
+ const versions = JSON.parse(readFileSync(file, "utf8"));
174
+ const { versions: next, changes } = applyRegistryLatest(versions, [{ registry, name, version }]);
175
+ if (JSON.stringify(next) !== JSON.stringify(versions)) writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
117
176
  const copies = syncConsumerCopies(root);
118
177
  log(`[right-release] release feed: ${name} ${version} ${changes.length ? "staged" : "already current"}${copies.length ? `; refreshed ${copies.length} copies` : ""}; commit rightkit-versions.json and its copies`);
119
178
  } catch (error) { log(`[right-release] release feed not updated (${error.message}); run node rightkit-sync.mjs`); }
@@ -143,8 +202,16 @@ export function syncConsumerCopies(root = rightkitRoot) {
143
202
  }
144
203
 
145
204
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
146
- const check = process.argv.includes("--check");
147
- syncRightkitVersions({ check })
148
- .then(({ changes }) => { if (check && changes.length) process.exitCode = 1; })
149
- .catch((error) => { process.stderr.write(`rightkit-sync: ${error.message}\n`); process.exitCode = 1; });
205
+ const main = async () => {
206
+ const args = process.argv.slice(2);
207
+ if (args[0] === "stage") {
208
+ if (args.length !== 4) throw new Error("usage: node rightkit-sync.mjs stage <npm|cargo> <name> <version>");
209
+ await stageVersion({ registry: args[1], name: args[2], version: args[3] });
210
+ return;
211
+ }
212
+ const check = args.includes("--check");
213
+ const { changes } = await syncRightkitVersions({ check });
214
+ if (check && changes.length) process.exitCode = 1;
215
+ };
216
+ main().catch((error) => { process.stderr.write(`rightkit-sync: ${error.message}\n`); process.exitCode = 1; });
150
217
  }
@@ -29,7 +29,7 @@
29
29
  "@rightkit/platform-ui": "0.1.2",
30
30
  "@rightkit/qa": "0.3.0",
31
31
  "@rightkit/qa-chrome": "0.1.3",
32
- "@rightkit/release": "0.2.128",
32
+ "@rightkit/release": "0.2.130",
33
33
  "@rightkit/settings-ui": "0.2.7",
34
34
  "@rightkit/shell": "0.1.0",
35
35
  "@rightkit/tauri": "0.1.1",
@@ -149,7 +149,11 @@
149
149
  "0.2.122",
150
150
  "0.2.123",
151
151
  "0.2.124",
152
- "0.2.125"
152
+ "0.2.125",
153
+ "0.2.126",
154
+ "0.2.127",
155
+ "0.2.128",
156
+ "0.2.129"
153
157
  ],
154
158
  "@rightkit/qa": [
155
159
  "0.1.0",
@@ -253,7 +257,7 @@
253
257
  "rightkit-qts": "0.2.1",
254
258
  "rightkit-qts-ggml": "0.1.0",
255
259
  "rightkit-qts-ggml-sys": "0.1.0",
256
- "rightkit-sandbox": "0.2.0",
260
+ "rightkit-sandbox": "0.2.1",
257
261
  "rightkit-search": "0.1.0",
258
262
  "rightkit-secrets": "0.1.0",
259
263
  "rightkit-service": "0.1.0",
@@ -303,7 +307,7 @@
303
307
  "rightkit-qts": "0.2.1",
304
308
  "rightkit-qts-ggml": "0.1.0",
305
309
  "rightkit-qts-ggml-sys": "0.1.0",
306
- "rightkit-sandbox": "0.2.0",
310
+ "rightkit-sandbox": "0.2.1",
307
311
  "rightkit-search": "0.1.0",
308
312
  "rightkit-secrets": "0.1.0",
309
313
  "rightkit-service": "0.1.0",
@@ -330,7 +334,7 @@
330
334
  "@rightkit/platform-ui": "0.1.2",
331
335
  "@rightkit/qa": "0.3.0",
332
336
  "@rightkit/qa-chrome": "0.1.3",
333
- "@rightkit/release": "0.2.127",
337
+ "@rightkit/release": "0.2.129",
334
338
  "@rightkit/settings-ui": "0.2.7",
335
339
  "@rightkit/shell": "0.1.0",
336
340
  "@rightkit/tauri": "0.1.1",
@@ -381,7 +385,8 @@
381
385
  "0.2.7"
382
386
  ],
383
387
  "rightkit-sandbox": [
384
- "0.1.0"
388
+ "0.1.0",
389
+ "0.2.0"
385
390
  ],
386
391
  "rightkit-shell": [
387
392
  "0.1.0",