alignfirst 0.9.0 → 0.10.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/README.md +1 -1
- package/dist/commands/doctor.js +10 -4
- package/dist/json-shape.d.ts +19 -0
- package/dist/json-shape.js +93 -0
- package/dist/project-config.js +26 -33
- package/dist/project-layout.js +15 -17
- package/dist/version-guard.js +2 -2
- package/dist/version-range.d.ts +5 -0
- package/dist/version-range.js +103 -0
- package/package.json +2 -5
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ The companion directory is `~/.alignfirst/companions/<name>`. The name is the ma
|
|
|
141
141
|
An agent reads a repository's `AGENTS.md` on its own, but never a companion. When your repositories carry no AlignFirst instructions, add this line to your global agent instructions (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, or the equivalent):
|
|
142
142
|
|
|
143
143
|
```text
|
|
144
|
-
In a git repository, run `alignfirst context` as your first command, whatever the task, unless the project's instructions already say so.
|
|
144
|
+
In a git repository, run `npx -y alignfirst context` as your first command, whatever the task, unless the project's instructions already say so.
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
## Upgrade from v1, v2, or v3
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import semver from "semver";
|
|
5
4
|
import { resolveDefaultBranch } from "../default-branch.js";
|
|
6
5
|
import { errorMessage } from "../errors.js";
|
|
7
6
|
import { parseBareCommandArgs } from "../parse-args.js";
|
|
@@ -11,6 +10,7 @@ import { PROJECT_CONFIG_FILENAME, resolveProjectConfig, } from "../project-confi
|
|
|
11
10
|
import { registryPath, sharedCompanionDirs, strayRegistryKeys, ITEM_NAMES, layoutOf, renderItemLocation, } from "../project-layout.js";
|
|
12
11
|
import { COMMAND_SKILLS, findInstalledSkill } from "../skills.js";
|
|
13
12
|
import { cliRangeResult } from "../version-guard.js";
|
|
13
|
+
import { isAheadOfRange } from "../version-range.js";
|
|
14
14
|
export function runDoctor(ctx, args) {
|
|
15
15
|
const usage = `Usage: ${ctx.form} doctor\n`;
|
|
16
16
|
if (parseBareCommandArgs(ctx, args, usage))
|
|
@@ -62,7 +62,7 @@ function inspectConfig(ctx, resolved) {
|
|
|
62
62
|
level: result.satisfied ? "ok" : "error",
|
|
63
63
|
text: `${result.satisfied ? "satisfies" : "does not satisfy"} ${result.range}`,
|
|
64
64
|
});
|
|
65
|
-
if (
|
|
65
|
+
if (isAheadOfRange(ctx.version, result.range))
|
|
66
66
|
lines.push({ level: "warn", text: `${ctx.version} is ahead of ${result.range}` });
|
|
67
67
|
return lines;
|
|
68
68
|
}
|
|
@@ -154,11 +154,17 @@ function describeCommandSkill({ name, installed }) {
|
|
|
154
154
|
}
|
|
155
155
|
function describeInstalledSkill(name, installed) {
|
|
156
156
|
const version = installed.version ?? "unknown";
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
157
|
+
const major = majorOf(installed.version);
|
|
158
|
+
if (major === undefined || major < 4)
|
|
159
159
|
return {
|
|
160
160
|
level: "warn",
|
|
161
161
|
text: `${name} ${version} predates v4; update: npx -y skills update --global --yes`,
|
|
162
162
|
};
|
|
163
163
|
return { level: "ok", text: `${name} ${version} (${installed.root})` };
|
|
164
164
|
}
|
|
165
|
+
function majorOf(version) {
|
|
166
|
+
if (version === undefined)
|
|
167
|
+
return;
|
|
168
|
+
const match = /^(\d+)\./.exec(version);
|
|
169
|
+
return match === null ? undefined : Number(match[1]);
|
|
170
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type Check<T> = (value: unknown, path: string) => T;
|
|
2
|
+
export interface OptionalField<T> {
|
|
3
|
+
optional: Check<T>;
|
|
4
|
+
}
|
|
5
|
+
type Fields<T> = {
|
|
6
|
+
[K in keyof T]-?: undefined extends T[K] ? OptionalField<Exclude<T[K], undefined>> : Check<T[K]>;
|
|
7
|
+
};
|
|
8
|
+
/** Runs `check` at the root; a shape failure becomes `toError(<message>)`. */
|
|
9
|
+
export declare function parseShape<T>(check: Check<T>, value: unknown, toError: (detail: string) => Error): T;
|
|
10
|
+
/** Rejects the first unknown key, then checks each field in declaration order. */
|
|
11
|
+
export declare function object<T extends object>(fields: Fields<T>): Check<T>;
|
|
12
|
+
export declare function shapeError(path: string, expectation: string): Error;
|
|
13
|
+
export declare function optional<T>(check: Check<T>): OptionalField<T>;
|
|
14
|
+
export declare function record<T>(check: Check<T>): Check<Record<string, T>>;
|
|
15
|
+
export declare function literal<const T extends readonly (string | number)[]>(...values: T): Check<T[number]>;
|
|
16
|
+
export declare function integerBetween(min: number, max: number): Check<number>;
|
|
17
|
+
export declare function nonEmptyString(value: unknown, path: string): string;
|
|
18
|
+
export declare function boolean(value: unknown, path: string): boolean;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
2
|
+
class ShapeError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Runs `check` at the root; a shape failure becomes `toError(<message>)`. */
|
|
5
|
+
export function parseShape(check, value, toError) {
|
|
6
|
+
try {
|
|
7
|
+
return check(value, "");
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof ShapeError)
|
|
11
|
+
throw toError(error.message);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Rejects the first unknown key, then checks each field in declaration order. */
|
|
16
|
+
export function object(fields) {
|
|
17
|
+
const entries = Object.entries(fields);
|
|
18
|
+
return (value, path) => {
|
|
19
|
+
if (!isPlainObject(value))
|
|
20
|
+
throw shapeError(path, "be an object");
|
|
21
|
+
const unknownKey = Object.keys(value).find((key) => !Object.hasOwn(fields, key));
|
|
22
|
+
if (unknownKey !== undefined)
|
|
23
|
+
throw shapeError(childPath(path, unknownKey), "be removed");
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const [key, field] of entries) {
|
|
26
|
+
const fieldPath = childPath(path, key);
|
|
27
|
+
if (typeof field === "function")
|
|
28
|
+
result[key] = field(value[key], fieldPath);
|
|
29
|
+
else if (value[key] !== undefined)
|
|
30
|
+
result[key] = field.optional(value[key], fieldPath);
|
|
31
|
+
}
|
|
32
|
+
// The fields mirror T key by key.
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
if (typeof value !== "object" || value === null)
|
|
38
|
+
return false;
|
|
39
|
+
const prototype = Object.getPrototypeOf(value);
|
|
40
|
+
return prototype === Object.prototype || prototype === null;
|
|
41
|
+
}
|
|
42
|
+
function childPath(path, key) {
|
|
43
|
+
if (!IDENTIFIER.test(key))
|
|
44
|
+
return bracketPath(path, JSON.stringify(key));
|
|
45
|
+
return path === "" ? key : `${path}.${key}`;
|
|
46
|
+
}
|
|
47
|
+
function bracketPath(path, segment) {
|
|
48
|
+
return `${path === "" ? "value" : path}[${segment}]`;
|
|
49
|
+
}
|
|
50
|
+
export function shapeError(path, expectation) {
|
|
51
|
+
return new ShapeError(`${path === "" ? "value" : path} must ${expectation}`);
|
|
52
|
+
}
|
|
53
|
+
export function optional(check) {
|
|
54
|
+
return { optional: check };
|
|
55
|
+
}
|
|
56
|
+
export function record(check) {
|
|
57
|
+
return (value, path) => {
|
|
58
|
+
if (!isPlainObject(value))
|
|
59
|
+
throw shapeError(path, "be an object");
|
|
60
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, check(item, childPath(path, key))]));
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function literal(...values) {
|
|
64
|
+
const expectation = `be ${listOf(values.map((item) => JSON.stringify(item)))}`;
|
|
65
|
+
return (value, path) => {
|
|
66
|
+
const match = values.find((item) => item === value);
|
|
67
|
+
if (match === undefined)
|
|
68
|
+
throw shapeError(path, expectation);
|
|
69
|
+
return match;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function listOf(items) {
|
|
73
|
+
if (items.length === 1)
|
|
74
|
+
return items[0];
|
|
75
|
+
return `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
|
|
76
|
+
}
|
|
77
|
+
export function integerBetween(min, max) {
|
|
78
|
+
return (value, path) => {
|
|
79
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max)
|
|
80
|
+
throw shapeError(path, `be an integer from ${min} to ${max}`);
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function nonEmptyString(value, path) {
|
|
85
|
+
if (typeof value !== "string" || value.length === 0)
|
|
86
|
+
throw shapeError(path, "be a non-empty string");
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
export function boolean(value, path) {
|
|
90
|
+
if (typeof value !== "boolean")
|
|
91
|
+
throw shapeError(path, "be a boolean");
|
|
92
|
+
return value;
|
|
93
|
+
}
|
package/dist/project-config.js
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { type } from "arktype";
|
|
3
|
-
import semver from "semver";
|
|
4
2
|
import { CliError } from "./cli-error.js";
|
|
5
3
|
import { errorMessage } from "./errors.js";
|
|
4
|
+
import { boolean, integerBetween, literal, nonEmptyString, object, optional, parseShape, } from "./json-shape.js";
|
|
5
|
+
import { isValidVersionRange } from "./version-range.js";
|
|
6
6
|
export const PROJECT_CONFIG_FILENAME = ".alignfirst.json";
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
last: "1 <= number.integer <= 65535",
|
|
7
|
+
const portRangeShape = object({
|
|
8
|
+
first: integerBetween(1, 65_535),
|
|
9
|
+
last: integerBetween(1, 65_535),
|
|
11
10
|
});
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"autoArchive?": "boolean",
|
|
11
|
+
const plansShape = object({
|
|
12
|
+
folder: optional(nonEmptyString),
|
|
13
|
+
autoArchive: optional(boolean),
|
|
16
14
|
});
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"ticketReference?": "'bracketed' | 'bracketedHash'",
|
|
15
|
+
const commitShape = object({
|
|
16
|
+
style: literal("conventionalCommit"),
|
|
17
|
+
ticketReference: optional(literal("bracketed", "bracketedHash")),
|
|
21
18
|
});
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"agentCoauthoring?": "boolean",
|
|
19
|
+
const gitShape = object({
|
|
20
|
+
defaultBranch: optional(nonEmptyString),
|
|
21
|
+
branchNameTemplate: optional(nonEmptyString),
|
|
22
|
+
commit: optional(commitShape),
|
|
23
|
+
agentCoauthoring: optional(boolean),
|
|
28
24
|
});
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"git?": gitSchema,
|
|
25
|
+
const projectConfigShape = object({
|
|
26
|
+
schemaVersion: literal(1),
|
|
27
|
+
cli: optional(nonEmptyString),
|
|
28
|
+
ticketIdPattern: optional(nonEmptyString),
|
|
29
|
+
plans: optional(plansShape),
|
|
30
|
+
portRange: optional(portRangeShape),
|
|
31
|
+
git: optional(gitShape),
|
|
37
32
|
});
|
|
38
33
|
export function resolveProjectConfig(layout) {
|
|
39
34
|
const location = layout.locations[PROJECT_CONFIG_FILENAME];
|
|
@@ -52,11 +47,9 @@ export function readProjectConfig(path) {
|
|
|
52
47
|
return validateProjectConfig(value, path);
|
|
53
48
|
}
|
|
54
49
|
export function validateProjectConfig(value, label) {
|
|
55
|
-
const config =
|
|
56
|
-
if (config
|
|
57
|
-
throw invalidConfig(label, config.
|
|
58
|
-
if (config.cli !== undefined && semver.validRange(config.cli) === null)
|
|
59
|
-
throw invalidConfig(label, `cli is not a valid semver range: ${config.cli}`);
|
|
50
|
+
const config = parseShape(projectConfigShape, value, (detail) => invalidConfig(label, detail));
|
|
51
|
+
if (config.cli !== undefined && !isValidVersionRange(config.cli))
|
|
52
|
+
throw invalidConfig(label, `cli is not a supported version range: ${config.cli}`);
|
|
60
53
|
if (config.ticketIdPattern !== undefined)
|
|
61
54
|
assertValidPattern(config.ticketIdPattern, label);
|
|
62
55
|
if (config.portRange !== undefined)
|
package/dist/project-layout.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { type } from "arktype";
|
|
4
3
|
import { CliError } from "./cli-error.js";
|
|
5
4
|
import { errorMessage } from "./errors.js";
|
|
6
5
|
import { gitOutputOrUndefined } from "./git.js";
|
|
6
|
+
import { object, optional, parseShape, record, shapeError } from "./json-shape.js";
|
|
7
7
|
export const ITEM_NAMES = [
|
|
8
8
|
".alignfirst.json",
|
|
9
9
|
".alignfirst-instructions",
|
|
@@ -15,20 +15,15 @@ export const ITEM_NAMES = [
|
|
|
15
15
|
// The directory holding the companions and their registry. A symlink there moves them elsewhere.
|
|
16
16
|
const COMPANIONS_ROOT = "~/.alignfirst/companions";
|
|
17
17
|
const REGISTRY_FILE = "registry.json";
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
".
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
".plans?": FLAG,
|
|
26
|
-
"_aligndev?": FLAG,
|
|
27
|
-
});
|
|
28
|
-
const registrySchema = type({
|
|
29
|
-
"+": "reject",
|
|
30
|
-
paths: type.Record("string", flagsSchema),
|
|
18
|
+
const flagsShape = object({
|
|
19
|
+
".alignfirst.json": optional(flag),
|
|
20
|
+
".alignfirst-instructions": optional(flag),
|
|
21
|
+
"DEVELOPERS.md": optional(flag),
|
|
22
|
+
docs: optional(flag),
|
|
23
|
+
".plans": optional(flag),
|
|
24
|
+
_aligndev: optional(flag),
|
|
31
25
|
});
|
|
26
|
+
const registryFileShape = object({ paths: record(flagsShape) });
|
|
32
27
|
export function layoutOf(ctx) {
|
|
33
28
|
ctx.layout ??= resolveProjectLayout(ctx.cwd, ctx.home);
|
|
34
29
|
return ctx.layout;
|
|
@@ -64,9 +59,7 @@ function readRegistry(home) {
|
|
|
64
59
|
catch (error) {
|
|
65
60
|
throw invalidRegistry(path, errorMessage(error));
|
|
66
61
|
}
|
|
67
|
-
const file =
|
|
68
|
-
if (file instanceof type.errors)
|
|
69
|
-
throw invalidRegistry(path, file.summary.split("\n", 1)[0]);
|
|
62
|
+
const file = parseShape(registryFileShape, value, (detail) => invalidRegistry(path, detail));
|
|
70
63
|
const badKey = Object.keys(file.paths).find((key) => !isUserPath(key));
|
|
71
64
|
if (badKey !== undefined)
|
|
72
65
|
throw invalidRegistry(path, `paths key must be an absolute path or start with ~/: ${badKey}`);
|
|
@@ -78,6 +71,11 @@ export function registryPath(home) {
|
|
|
78
71
|
function invalidRegistry(path, detail) {
|
|
79
72
|
return new CliError(`Invalid ${path}: ${detail}`);
|
|
80
73
|
}
|
|
74
|
+
function flag(value, path) {
|
|
75
|
+
if (typeof value !== "boolean" && value !== "auto")
|
|
76
|
+
throw shapeError(path, 'be a boolean or "auto"');
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
81
79
|
function isUserPath(value) {
|
|
82
80
|
return value === "~" || value.startsWith("~/") || isAbsolute(value);
|
|
83
81
|
}
|
package/dist/version-guard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import semver from "semver";
|
|
2
1
|
import { CliError } from "./cli-error.js";
|
|
2
|
+
import { satisfiesRange } from "./version-range.js";
|
|
3
3
|
export function checkCliRange(config, installedVersion, commandArgs) {
|
|
4
4
|
const result = cliRangeResult(config, installedVersion);
|
|
5
5
|
if (result === undefined || result.satisfied)
|
|
@@ -13,5 +13,5 @@ export function cliRangeResult(config, installedVersion) {
|
|
|
13
13
|
const range = config?.cli;
|
|
14
14
|
if (range === undefined)
|
|
15
15
|
return;
|
|
16
|
-
return { range, satisfied:
|
|
16
|
+
return { range, satisfied: satisfiesRange(installedVersion, range) };
|
|
17
17
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function isValidVersionRange(range: string): boolean;
|
|
2
|
+
/** False for a version with a prerelease tag or an invalid range. */
|
|
3
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|
|
4
|
+
/** True when `version` is above every version the range accepts. */
|
|
5
|
+
export declare function isAheadOfRange(version: string, range: string): boolean;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
2
|
+
// Two-character operators first, so `<=` is not read as `<`.
|
|
3
|
+
const OPERATORS = ["<=", ">=", "<", ">", "=", "~", "^"];
|
|
4
|
+
export function isValidVersionRange(range) {
|
|
5
|
+
return parseRange(range) !== undefined;
|
|
6
|
+
}
|
|
7
|
+
/** False for a version with a prerelease tag or an invalid range. */
|
|
8
|
+
export function satisfiesRange(version, range) {
|
|
9
|
+
const parsed = parseVersion(version);
|
|
10
|
+
const sets = parseRange(range);
|
|
11
|
+
if (parsed === undefined || sets === undefined)
|
|
12
|
+
return false;
|
|
13
|
+
return sets.some((set) => set.every((comparator) => holds(comparator, parsed)));
|
|
14
|
+
}
|
|
15
|
+
/** True when `version` is above every version the range accepts. */
|
|
16
|
+
export function isAheadOfRange(version, range) {
|
|
17
|
+
const parsed = parseVersion(version);
|
|
18
|
+
const sets = parseRange(range);
|
|
19
|
+
if (parsed === undefined || sets === undefined)
|
|
20
|
+
return false;
|
|
21
|
+
return sets.every((set) => set.some((comparator) => exceeds(comparator, parsed)));
|
|
22
|
+
}
|
|
23
|
+
function parseVersion(text) {
|
|
24
|
+
const match = VERSION.exec(text);
|
|
25
|
+
if (match === null)
|
|
26
|
+
return;
|
|
27
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
|
|
28
|
+
}
|
|
29
|
+
function parseRange(range) {
|
|
30
|
+
const sets = [];
|
|
31
|
+
for (const part of range.split("||")) {
|
|
32
|
+
const set = parseComparatorSet(part.trim());
|
|
33
|
+
if (set === undefined)
|
|
34
|
+
return;
|
|
35
|
+
sets.push(set);
|
|
36
|
+
}
|
|
37
|
+
return sets;
|
|
38
|
+
}
|
|
39
|
+
function parseComparatorSet(text) {
|
|
40
|
+
if (text === "")
|
|
41
|
+
return;
|
|
42
|
+
const comparators = [];
|
|
43
|
+
for (const token of text.split(/\s+/)) {
|
|
44
|
+
const desugared = parseComparator(token);
|
|
45
|
+
if (desugared === undefined)
|
|
46
|
+
return;
|
|
47
|
+
comparators.push(...desugared);
|
|
48
|
+
}
|
|
49
|
+
return comparators;
|
|
50
|
+
}
|
|
51
|
+
function parseComparator(token) {
|
|
52
|
+
const operator = OPERATORS.find((candidate) => token.startsWith(candidate));
|
|
53
|
+
const version = parseVersion(operator === undefined ? token : token.slice(operator.length));
|
|
54
|
+
if (version === undefined)
|
|
55
|
+
return;
|
|
56
|
+
return desugar(operator ?? "=", version);
|
|
57
|
+
}
|
|
58
|
+
function desugar(operator, version) {
|
|
59
|
+
if (operator === "~") {
|
|
60
|
+
const upper = { major: version.major, minor: version.minor + 1, patch: 0 };
|
|
61
|
+
return [lowerBound(version), { operator: "<", version: upper }];
|
|
62
|
+
}
|
|
63
|
+
if (operator === "^")
|
|
64
|
+
return [lowerBound(version), { operator: "<", version: caretUpper(version) }];
|
|
65
|
+
return [{ operator, version }];
|
|
66
|
+
}
|
|
67
|
+
function lowerBound(version) {
|
|
68
|
+
return { operator: ">=", version };
|
|
69
|
+
}
|
|
70
|
+
function caretUpper({ major, minor, patch }) {
|
|
71
|
+
if (major > 0)
|
|
72
|
+
return { major: major + 1, minor: 0, patch: 0 };
|
|
73
|
+
if (minor > 0)
|
|
74
|
+
return { major: 0, minor: minor + 1, patch: 0 };
|
|
75
|
+
return { major: 0, minor: 0, patch: patch + 1 };
|
|
76
|
+
}
|
|
77
|
+
function holds({ operator, version }, candidate) {
|
|
78
|
+
const order = compareVersions(candidate, version);
|
|
79
|
+
switch (operator) {
|
|
80
|
+
case "<":
|
|
81
|
+
return order < 0;
|
|
82
|
+
case "<=":
|
|
83
|
+
return order <= 0;
|
|
84
|
+
case ">":
|
|
85
|
+
return order > 0;
|
|
86
|
+
case ">=":
|
|
87
|
+
return order >= 0;
|
|
88
|
+
case "=":
|
|
89
|
+
return order === 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function compareVersions(left, right) {
|
|
93
|
+
return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
|
|
94
|
+
}
|
|
95
|
+
/** A set is exceeded when the candidate fails one of its upper bounds on the high side. */
|
|
96
|
+
function exceeds({ operator, version }, candidate) {
|
|
97
|
+
const order = compareVersions(candidate, version);
|
|
98
|
+
if (operator === "<")
|
|
99
|
+
return order >= 0;
|
|
100
|
+
if (operator === "<=" || operator === "=")
|
|
101
|
+
return order > 0;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alignfirst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "CC0-1.0",
|
|
5
5
|
"author": "Thomas MUR",
|
|
6
6
|
"description": "The AlignFirst CLI: protocols, work files and docs in one command.",
|
|
@@ -35,13 +35,10 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@alignfirst/docmap": "~0.11.1"
|
|
39
|
-
"arktype": "^2.2.3",
|
|
40
|
-
"semver": "^7.8.5"
|
|
38
|
+
"@alignfirst/docmap": "~0.11.1"
|
|
41
39
|
},
|
|
42
40
|
"devDependencies": {
|
|
43
41
|
"@types/node": "~24.13.3",
|
|
44
|
-
"@types/semver": "~7.8.0",
|
|
45
42
|
"rimraf": "~6.1.3",
|
|
46
43
|
"typescript": "~7.0.2",
|
|
47
44
|
"vitest": "~5.0.1"
|