@tikoci/quickchr 0.4.1 → 0.4.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { Arch, Channel } from "./types.ts";
6
- import { QuickCHRError } from "./types.ts";
6
+ import { CHANNELS, QuickCHRError } from "./types.ts";
7
7
  import { fetchResilient } from "./net.ts";
8
8
 
9
9
  const UPGRADE_BASE = "https://upgrade.mikrotik.com/routeros/NEWESTa7";
@@ -92,9 +92,8 @@ export async function resolveVersion(channel: Channel): Promise<string> {
92
92
 
93
93
  /** Fetch latest versions for all channels in parallel. */
94
94
  export async function resolveAllVersions(): Promise<Record<Channel, string>> {
95
- const channels: Channel[] = ["stable", "long-term", "testing", "development"];
96
95
  const results = await Promise.all(
97
- channels.map(async (ch) => [ch, await resolveVersion(ch)] as const),
96
+ CHANNELS.map(async (ch) => [ch, await resolveVersion(ch)] as const),
98
97
  );
99
98
  return Object.fromEntries(results) as Record<Channel, string>;
100
99
  }
@@ -104,27 +103,58 @@ export function isValidVersion(version: string): boolean {
104
103
  return /^\d+\.\d+(\.\d+)?(beta\d+|rc\d+)?$/.test(version);
105
104
  }
106
105
 
107
- /** Parse a RouterOS version into numeric parts. beta/rc suffixes are ignored for ordering. */
108
- export function parseVersionParts(version: string): [number, number, number] {
109
- const match = version.match(/^(\d+)\.(\d+)(?:\.(\d+))?(?:beta\d+|rc\d+)?$/);
106
+ /**
107
+ * RouterOS pre-release ordering rank. For a given X.Y, a beta is older than an rc,
108
+ * which is older than the final release: beta < rc < release.
109
+ */
110
+ const STAGE_RANK = { beta: 0, rc: 1, release: 2 } as const;
111
+
112
+ interface ParsedVersion {
113
+ major: number;
114
+ minor: number;
115
+ patch: number;
116
+ /** Maturity rank within X.Y: beta=0 < rc=1 < release=2. */
117
+ stage: (typeof STAGE_RANK)[keyof typeof STAGE_RANK];
118
+ /** The N in betaN/rcN (0 for release builds). */
119
+ stageNum: number;
120
+ }
121
+
122
+ /** Parse a RouterOS version into numeric parts plus pre-release stage. */
123
+ function parseVersion(version: string): ParsedVersion {
124
+ const match = version.match(/^(\d+)\.(\d+)(?:\.(\d+))?(?:(beta|rc)(\d+))?$/);
110
125
  if (!match) {
111
126
  throw new QuickCHRError("INVALID_VERSION", `Invalid version: ${version}`);
112
127
  }
113
128
 
114
- const major = Number.parseInt(match[1] ?? "0", 10);
115
- const minor = Number.parseInt(match[2] ?? "0", 10);
116
- const patch = Number.parseInt(match[3] ?? "0", 10);
129
+ const stageName = match[4] as "beta" | "rc" | undefined;
130
+ return {
131
+ major: Number.parseInt(match[1] ?? "0", 10),
132
+ minor: Number.parseInt(match[2] ?? "0", 10),
133
+ patch: Number.parseInt(match[3] ?? "0", 10),
134
+ stage: stageName ? STAGE_RANK[stageName] : STAGE_RANK.release,
135
+ stageNum: match[5] ? Number.parseInt(match[5], 10) : 0,
136
+ };
137
+ }
138
+
139
+ /** Parse a RouterOS version into numeric parts. beta/rc suffixes are ignored. */
140
+ export function parseVersionParts(version: string): [number, number, number] {
141
+ const { major, minor, patch } = parseVersion(version);
117
142
  return [major, minor, patch];
118
143
  }
119
144
 
120
- /** Compare two RouterOS versions semantically. */
145
+ /**
146
+ * Compare two RouterOS versions semantically, including pre-release suffixes:
147
+ * `7.24beta2` < `7.24rc1` < `7.24` < `7.24.1`. Returns <0, 0, or >0.
148
+ */
121
149
  export function compareRouterOsVersion(left: string, right: string): number {
122
- const [leftMajor, leftMinor, leftPatch] = parseVersionParts(left);
123
- const [rightMajor, rightMinor, rightPatch] = parseVersionParts(right);
150
+ const a = parseVersion(left);
151
+ const b = parseVersion(right);
124
152
 
125
- if (leftMajor !== rightMajor) return leftMajor - rightMajor;
126
- if (leftMinor !== rightMinor) return leftMinor - rightMinor;
127
- return leftPatch - rightPatch;
153
+ if (a.major !== b.major) return a.major - b.major;
154
+ if (a.minor !== b.minor) return a.minor - b.minor;
155
+ if (a.patch !== b.patch) return a.patch - b.patch;
156
+ if (a.stage !== b.stage) return a.stage - b.stage;
157
+ return a.stageNum - b.stageNum;
128
158
  }
129
159
 
130
160
  /** True if RouterOS version is supported for quickchr provisioning mutations. */
@@ -180,3 +210,65 @@ export function generateMachineName(
180
210
  }
181
211
  return `${base}-${n}`;
182
212
  }
213
+
214
+ // --- Channel recency ---
215
+
216
+ /** Channels that ship as finished releases vs. moving pre-release builds. */
217
+ const RELEASED_CHANNELS: readonly Channel[] = ["stable", "long-term"];
218
+
219
+ export type ChannelMaturity = "released" | "prerelease";
220
+
221
+ /** Classify a channel as a finished release line or a pre-release line. */
222
+ export function channelMaturity(channel: Channel): ChannelMaturity {
223
+ return RELEASED_CHANNELS.includes(channel) ? "released" : "prerelease";
224
+ }
225
+
226
+ export interface ChannelStatus {
227
+ channel: Channel;
228
+ /** Resolved version for the channel, e.g. "7.24beta2". */
229
+ version: string;
230
+ maturity: ChannelMaturity;
231
+ /** True if this channel's version is at or ahead of the current stable release. */
232
+ aheadOfStable: boolean;
233
+ }
234
+
235
+ /**
236
+ * Classify each channel by current recency (pure — no network).
237
+ * `aheadOfStable` is always measured against the `stable` version in `versions`.
238
+ */
239
+ export function classifyChannels(versions: Record<Channel, string>): ChannelStatus[] {
240
+ const stableVersion = versions.stable;
241
+ return CHANNELS.map((channel) => ({
242
+ channel,
243
+ version: versions[channel],
244
+ maturity: channelMaturity(channel),
245
+ aheadOfStable: compareRouterOsVersion(versions[channel], stableVersion) >= 0,
246
+ }));
247
+ }
248
+
249
+ /**
250
+ * Select the channels currently worth booting (pure — no network): every released
251
+ * channel unconditionally, plus any pre-release channel at or ahead of the reference
252
+ * channel's version (default reference: `stable`). This answers "what's worth booting,"
253
+ * never "what must pass" — gate policy stays with the consumer.
254
+ */
255
+ export function selectActiveChannels(
256
+ versions: Record<Channel, string>,
257
+ opts: { aheadOf?: Channel } = {},
258
+ ): Channel[] {
259
+ const referenceVersion = versions[opts.aheadOf ?? "stable"];
260
+ return CHANNELS.filter((channel) => {
261
+ if (channelMaturity(channel) === "released") return true;
262
+ return compareRouterOsVersion(versions[channel], referenceVersion) >= 0;
263
+ });
264
+ }
265
+
266
+ /** Fetch all channel versions and classify each by current recency. */
267
+ export async function resolveChannelStatuses(): Promise<ChannelStatus[]> {
268
+ return classifyChannels(await resolveAllVersions());
269
+ }
270
+
271
+ /** Fetch all channel versions and return the channels currently worth booting. */
272
+ export async function resolveActiveChannels(opts: { aheadOf?: Channel } = {}): Promise<Channel[]> {
273
+ return selectActiveChannels(await resolveAllVersions(), opts);
274
+ }