@rightkit/release 0.2.129 → 0.2.131

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.129",
3
+ "version": "0.2.131",
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.129",
32
+ "@rightkit/release": "0.2.131",
33
33
  "@rightkit/settings-ui": "0.2.7",
34
34
  "@rightkit/shell": "0.1.0",
35
35
  "@rightkit/tauri": "0.1.1",
@@ -152,7 +152,9 @@
152
152
  "0.2.125",
153
153
  "0.2.126",
154
154
  "0.2.127",
155
- "0.2.128"
155
+ "0.2.128",
156
+ "0.2.129",
157
+ "0.2.130"
156
158
  ],
157
159
  "@rightkit/qa": [
158
160
  "0.1.0",
@@ -234,7 +236,7 @@
234
236
  "rightkit-automation": "0.1.0",
235
237
  "rightkit-browser": "0.2.2",
236
238
  "rightkit-cache": "0.1.0",
237
- "rightkit-control": "0.1.6",
239
+ "rightkit-control": "0.1.7",
238
240
  "rightkit-crash": "0.1.0",
239
241
  "rightkit-db": "0.3.0",
240
242
  "rightkit-embed": "0.2.2",
@@ -252,7 +254,7 @@
252
254
  "rightkit-pdf": "0.1.0",
253
255
  "rightkit-playback": "0.1.1",
254
256
  "rightkit-process": "0.3.0",
255
- "rightkit-qa": "0.2.8",
257
+ "rightkit-qa": "0.2.9",
256
258
  "rightkit-qts": "0.2.1",
257
259
  "rightkit-qts-ggml": "0.1.0",
258
260
  "rightkit-qts-ggml-sys": "0.1.0",
@@ -284,7 +286,7 @@
284
286
  "rightkit-audio": "0.1.1",
285
287
  "rightkit-browser": "0.2.2",
286
288
  "rightkit-cache": "0.1.0",
287
- "rightkit-control": "0.1.6",
289
+ "rightkit-control": "0.1.7",
288
290
  "rightkit-crash": "0.1.0",
289
291
  "rightkit-db": "0.3.0",
290
292
  "rightkit-embed": "0.2.2",
@@ -302,7 +304,7 @@
302
304
  "rightkit-pdf": "0.1.0",
303
305
  "rightkit-playback": "0.1.1",
304
306
  "rightkit-process": "0.3.0",
305
- "rightkit-qa": "0.2.8",
307
+ "rightkit-qa": "0.2.9",
306
308
  "rightkit-qts": "0.2.1",
307
309
  "rightkit-qts-ggml": "0.1.0",
308
310
  "rightkit-qts-ggml-sys": "0.1.0",
@@ -333,7 +335,7 @@
333
335
  "@rightkit/platform-ui": "0.1.2",
334
336
  "@rightkit/qa": "0.3.0",
335
337
  "@rightkit/qa-chrome": "0.1.3",
336
- "@rightkit/release": "0.2.128",
338
+ "@rightkit/release": "0.2.130",
337
339
  "@rightkit/settings-ui": "0.2.7",
338
340
  "@rightkit/shell": "0.1.0",
339
341
  "@rightkit/tauri": "0.1.1",
@@ -381,7 +383,8 @@
381
383
  "0.2.1"
382
384
  ],
383
385
  "rightkit-qa": [
384
- "0.2.7"
386
+ "0.2.7",
387
+ "0.2.8"
385
388
  ],
386
389
  "rightkit-sandbox": [
387
390
  "0.1.0",
@@ -397,6 +400,9 @@
397
400
  ],
398
401
  "rightkit-qts": [
399
402
  "0.2.0"
403
+ ],
404
+ "rightkit-control": [
405
+ "0.1.6"
400
406
  ]
401
407
  }
402
408
  }