@carllee1983/tagsmith 0.2.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli/check.d.ts +5 -0
  4. package/dist/cli/check.js +83 -0
  5. package/dist/cli/create.d.ts +10 -0
  6. package/dist/cli/create.js +75 -0
  7. package/dist/cli/guidance.d.ts +14 -0
  8. package/dist/cli/guidance.js +43 -0
  9. package/dist/cli/guide.d.ts +17 -0
  10. package/dist/cli/guide.js +61 -0
  11. package/dist/cli/implicit.d.ts +5 -0
  12. package/dist/cli/implicit.js +20 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +96 -0
  15. package/dist/cli/init.d.ts +11 -0
  16. package/dist/cli/init.js +127 -0
  17. package/dist/cli/list.d.ts +6 -0
  18. package/dist/cli/list.js +124 -0
  19. package/dist/cli/next.d.ts +7 -0
  20. package/dist/cli/next.js +57 -0
  21. package/dist/cli/resolve-config.d.ts +6 -0
  22. package/dist/cli/resolve-config.js +16 -0
  23. package/dist/cli/ui.d.ts +8 -0
  24. package/dist/cli/ui.js +16 -0
  25. package/dist/core/analyze.d.ts +21 -0
  26. package/dist/core/analyze.js +55 -0
  27. package/dist/core/check.d.ts +15 -0
  28. package/dist/core/check.js +27 -0
  29. package/dist/core/config.d.ts +21 -0
  30. package/dist/core/config.js +152 -0
  31. package/dist/core/defaults.d.ts +6 -0
  32. package/dist/core/defaults.js +16 -0
  33. package/dist/core/infer.d.ts +6 -0
  34. package/dist/core/infer.js +35 -0
  35. package/dist/core/lines.d.ts +13 -0
  36. package/dist/core/lines.js +27 -0
  37. package/dist/core/models/build.d.ts +13 -0
  38. package/dist/core/models/build.js +33 -0
  39. package/dist/core/models/calver.d.ts +19 -0
  40. package/dist/core/models/calver.js +166 -0
  41. package/dist/core/models/index.d.ts +10 -0
  42. package/dist/core/models/index.js +21 -0
  43. package/dist/core/models/semver.d.ts +13 -0
  44. package/dist/core/models/semver.js +57 -0
  45. package/dist/core/pattern.d.ts +15 -0
  46. package/dist/core/pattern.js +29 -0
  47. package/dist/core/plan.d.ts +33 -0
  48. package/dist/core/plan.js +59 -0
  49. package/dist/git/git.d.ts +23 -0
  50. package/dist/git/git.js +47 -0
  51. package/dist/types.d.ts +66 -0
  52. package/dist/types.js +1 -0
  53. package/package.json +60 -0
  54. package/schema.json +59 -0
@@ -0,0 +1,33 @@
1
+ import type { BumpLevel, TagLine, VersionModel } from "../types.js";
2
+ import { type Analysis } from "./analyze.js";
3
+ export interface NextPlan {
4
+ /** The full next tag name, e.g. "v1.2.4". */
5
+ tag: string;
6
+ /** The next version string (without pattern), e.g. "1.2.4". */
7
+ version: string;
8
+ /** The version this was derived from, or null when starting fresh. */
9
+ fromVersion: string | null;
10
+ /** Whether the initialVersion was used (no prior conforming tag). */
11
+ fresh: boolean;
12
+ analysis: Analysis;
13
+ }
14
+ export declare class PlanError extends Error {
15
+ }
16
+ /**
17
+ * Compute the next tag for the repo given existing tags. Guarantees the result
18
+ * is strictly greater than the current highest conforming version.
19
+ */
20
+ export declare function planNext(line: TagLine, model: VersionModel, existingTags: readonly string[], level?: BumpLevel): NextPlan;
21
+ export interface ValidateOptions {
22
+ allowOutOfOrder?: boolean;
23
+ }
24
+ export interface ValidationResult {
25
+ ok: boolean;
26
+ errors: string[];
27
+ }
28
+ /**
29
+ * Validate that an explicit version is safe to create: it must parse, render
30
+ * to a tag that does not already exist, and (unless overridden) be strictly
31
+ * greater than the current highest conforming version.
32
+ */
33
+ export declare function validateExplicit(line: TagLine, model: VersionModel, explicitVersion: string, existingTags: readonly string[], opts?: ValidateOptions): ValidationResult;
@@ -0,0 +1,59 @@
1
+ import { compilePattern } from "./pattern.js";
2
+ import { analyzeTags } from "./analyze.js";
3
+ export class PlanError extends Error {
4
+ }
5
+ /**
6
+ * Compute the next tag for the repo given existing tags. Guarantees the result
7
+ * is strictly greater than the current highest conforming version.
8
+ */
9
+ export function planNext(line, model, existingTags, level = "patch") {
10
+ const pattern = compilePattern(line.pattern);
11
+ const analysis = analyzeTags(existingTags, pattern, model);
12
+ if (analysis.latest === null) {
13
+ const initial = model.initial(line.initialVersion);
14
+ return {
15
+ tag: pattern.render(model.format(initial)),
16
+ version: model.format(initial),
17
+ fromVersion: null,
18
+ fresh: true,
19
+ analysis,
20
+ };
21
+ }
22
+ const current = analysis.latest.version;
23
+ const next = model.bump(current, level);
24
+ if (model.compare(next, current) <= 0) {
25
+ throw new PlanError(`Bumping ${model.format(current)} by "${level}" did not increase the version.`);
26
+ }
27
+ return {
28
+ tag: pattern.render(model.format(next)),
29
+ version: model.format(next),
30
+ fromVersion: model.format(current),
31
+ fresh: false,
32
+ analysis,
33
+ };
34
+ }
35
+ /**
36
+ * Validate that an explicit version is safe to create: it must parse, render
37
+ * to a tag that does not already exist, and (unless overridden) be strictly
38
+ * greater than the current highest conforming version.
39
+ */
40
+ export function validateExplicit(line, model, explicitVersion, existingTags, opts = {}) {
41
+ const pattern = compilePattern(line.pattern);
42
+ const errors = [];
43
+ const parsed = model.parse(explicitVersion);
44
+ if (parsed === null) {
45
+ errors.push(`Version "${explicitVersion}" is not valid for the ${model.type} model.`);
46
+ return { ok: false, errors };
47
+ }
48
+ const tagName = pattern.render(model.format(parsed));
49
+ if (existingTags.includes(tagName)) {
50
+ errors.push(`Tag "${tagName}" already exists.`);
51
+ }
52
+ const analysis = analyzeTags(existingTags, pattern, model);
53
+ if (analysis.latest !== null && !opts.allowOutOfOrder) {
54
+ if (model.compare(parsed, analysis.latest.version) <= 0) {
55
+ errors.push(`Version "${model.format(parsed)}" is not greater than the latest "${model.format(analysis.latest.version)}". Use --allow-out-of-order to override.`);
56
+ }
57
+ }
58
+ return { ok: errors.length === 0, errors };
59
+ }
@@ -0,0 +1,23 @@
1
+ export declare class GitError extends Error {
2
+ }
3
+ export interface GitOptions {
4
+ /** Working directory for git commands. */
5
+ cwd: string;
6
+ }
7
+ /** Throw a friendly error when cwd is not inside a git work tree. */
8
+ export declare function ensureRepo(opts: GitOptions): Promise<void>;
9
+ /** List all tag names in the repo. */
10
+ export declare function listTags(opts: GitOptions): Promise<string[]>;
11
+ export interface CreateTagOptions extends GitOptions {
12
+ name: string;
13
+ /** When provided, creates an annotated tag with this message. */
14
+ message?: string;
15
+ /** Target ref/commit; defaults to HEAD. */
16
+ ref?: string;
17
+ }
18
+ export declare function createTag(opts: CreateTagOptions): Promise<void>;
19
+ export interface PushTagOptions extends GitOptions {
20
+ name: string;
21
+ remote?: string;
22
+ }
23
+ export declare function pushTag(opts: PushTagOptions): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export class GitError extends Error {
5
+ }
6
+ async function git(args, cwd) {
7
+ try {
8
+ const { stdout } = await execFileAsync("git", args, { cwd });
9
+ return stdout;
10
+ }
11
+ catch (err) {
12
+ const e = err;
13
+ throw new GitError(`git ${args.join(" ")} failed: ${(e.stderr ?? e.message ?? "unknown error").trim()}`);
14
+ }
15
+ }
16
+ /** Throw a friendly error when cwd is not inside a git work tree. */
17
+ export async function ensureRepo(opts) {
18
+ try {
19
+ await git(["rev-parse", "--is-inside-work-tree"], opts.cwd);
20
+ }
21
+ catch {
22
+ throw new GitError(`Not a git repository: ${opts.cwd}. Run \`git init\` first.`);
23
+ }
24
+ }
25
+ /** List all tag names in the repo. */
26
+ export async function listTags(opts) {
27
+ const out = await git(["tag", "--list"], opts.cwd);
28
+ return out
29
+ .split("\n")
30
+ .map((l) => l.trim())
31
+ .filter((l) => l.length > 0);
32
+ }
33
+ export async function createTag(opts) {
34
+ const args = ["tag"];
35
+ if (opts.message !== undefined) {
36
+ args.push("-a", opts.name, "-m", opts.message);
37
+ }
38
+ else {
39
+ args.push(opts.name);
40
+ }
41
+ if (opts.ref !== undefined)
42
+ args.push(opts.ref);
43
+ await git(args, opts.cwd);
44
+ }
45
+ export async function pushTag(opts) {
46
+ await git(["push", opts.remote ?? "origin", opts.name], opts.cwd);
47
+ }
@@ -0,0 +1,66 @@
1
+ /** Bump levels understood by version models. Not every model supports every level. */
2
+ export type BumpLevel = "major" | "minor" | "patch" | "prerelease" | "auto";
3
+ /**
4
+ * A version model knows how to parse, compare, format and bump a particular
5
+ * versioning scheme (semver, calver, build number…). Implementations are pure:
6
+ * no IO, no clock access (the current date is injected where needed).
7
+ */
8
+ export interface VersionModel<V = unknown> {
9
+ readonly type: string;
10
+ /** Parse a raw version string; return null when it does not conform. */
11
+ parse(raw: string): V | null;
12
+ /** Standard comparator: negative if a<b, 0 if equal, positive if a>b. */
13
+ compare(a: V, b: V): number;
14
+ /** Render a parsed version back to its canonical string. */
15
+ format(v: V): string;
16
+ /** Produce the next version for the given level. */
17
+ bump(v: V, level: BumpLevel): V;
18
+ /** Build the starting version from the config's initialVersion. */
19
+ initial(raw: string): V;
20
+ }
21
+ export type ModelType = "semver" | "calver" | "build";
22
+ export interface SemverModelConfig {
23
+ type: "semver";
24
+ allowPrerelease?: boolean;
25
+ }
26
+ export interface CalverModelConfig {
27
+ type: "calver";
28
+ /** Format tokens: YYYY, YY, MM, DD, MICRO. e.g. "YYYY.MM.MICRO". */
29
+ format: string;
30
+ }
31
+ export interface BuildModelConfig {
32
+ type: "build";
33
+ /** Zero-pad the number to this width (0 = no padding). */
34
+ padding?: number;
35
+ }
36
+ export type ModelConfig = SemverModelConfig | CalverModelConfig | BuildModelConfig;
37
+ /** 一條獨立的 tag 線:有自己的 pattern、版本模型、起始版本與 push 設定。 */
38
+ export interface TagLine {
39
+ /** 線名,唯一,用於 CLI 選線。 */
40
+ name: string;
41
+ /** Tag 模板;MUST 含且僅含一個 `{version}`。 */
42
+ pattern: string;
43
+ model: ModelConfig;
44
+ /** 無對應 tag 時的起始版本。 */
45
+ initialVersion: string;
46
+ /** 建立時是否預設 push。 */
47
+ push: boolean;
48
+ }
49
+ /** 內部正規化後的設定:一律為多線結構。 */
50
+ export interface TagsmithConfig {
51
+ lines: TagLine[];
52
+ /** 預設線名,正規化後一定指向有效的 line.name。 */
53
+ default: string;
54
+ }
55
+ /** A tag parsed against the configured pattern + model. */
56
+ export interface ParsedTag {
57
+ /** Raw git tag name. */
58
+ raw: string;
59
+ /** Version substring extracted by the pattern, when it matched. */
60
+ versionString: string | null;
61
+ /** Parsed version value (model-specific), when parseable. */
62
+ version: unknown | null;
63
+ /** Why a tag is considered non-conforming, if so. */
64
+ anomaly: TagAnomaly | null;
65
+ }
66
+ export type TagAnomaly = "pattern-mismatch" | "unparseable-version" | "duplicate-version";
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@carllee1983/tagsmith",
3
+ "version": "0.2.0",
4
+ "description": "Define git tag specs, view tags, and generate the next git tag safely — avoiding ordering or format anomalies.",
5
+ "type": "module",
6
+ "publishConfig": { "access": "public" },
7
+ "bin": {
8
+ "tagsmith": "dist/cli/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "schema.json"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "dev": "tsx src/cli/index.ts",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "coverage": "vitest run --coverage",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "keywords": [
24
+ "git",
25
+ "tag",
26
+ "semver",
27
+ "calver",
28
+ "cli",
29
+ "release",
30
+ "versioning"
31
+ ],
32
+ "license": "MIT",
33
+ "author": "carl",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/CarlLee1983/Tagsmith.git"
37
+ },
38
+ "homepage": "https://github.com/CarlLee1983/Tagsmith#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/CarlLee1983/Tagsmith/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "@clack/prompts": "^0.7.0",
47
+ "commander": "^12.1.0",
48
+ "picocolors": "^1.0.1",
49
+ "semver": "^7.6.3",
50
+ "zod": "^3.23.8"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.5.0",
54
+ "@types/semver": "^7.5.8",
55
+ "@vitest/coverage-v8": "^2.0.5",
56
+ "tsx": "^4.19.0",
57
+ "typescript": "^5.5.4",
58
+ "vitest": "^2.0.5"
59
+ }
60
+ }
package/schema.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/CMG/tagsmith/schema.json",
4
+ "title": "Tagsmith config",
5
+ "description": "Tag specification for the tagsmith CLI (.tagsmith.json).",
6
+ "type": "object",
7
+ "required": ["pattern", "model", "initialVersion"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "pattern": {
12
+ "type": "string",
13
+ "description": "Tag template; must contain the {version} placeholder.",
14
+ "pattern": "\\{version\\}"
15
+ },
16
+ "model": {
17
+ "oneOf": [
18
+ {
19
+ "type": "object",
20
+ "required": ["type"],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "type": { "const": "semver" },
24
+ "allowPrerelease": { "type": "boolean" }
25
+ }
26
+ },
27
+ {
28
+ "type": "object",
29
+ "required": ["type", "format"],
30
+ "additionalProperties": false,
31
+ "properties": {
32
+ "type": { "const": "calver" },
33
+ "format": {
34
+ "type": "string",
35
+ "description": "Token sequence using YYYY YY MM DD MICRO, e.g. YYYY.MM.MICRO."
36
+ }
37
+ }
38
+ },
39
+ {
40
+ "type": "object",
41
+ "required": ["type"],
42
+ "additionalProperties": false,
43
+ "properties": {
44
+ "type": { "const": "build" },
45
+ "padding": { "type": "integer", "minimum": 0 }
46
+ }
47
+ }
48
+ ]
49
+ },
50
+ "initialVersion": {
51
+ "type": "string",
52
+ "description": "Version used when no conforming tag exists yet."
53
+ },
54
+ "push": {
55
+ "type": "boolean",
56
+ "description": "Default push behaviour for `tagsmith create`."
57
+ }
58
+ }
59
+ }