@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,20 @@
1
+ import { color, info } from "./ui.js";
2
+ /** Extra JSON fields when config was inferred rather than loaded from disk. */
3
+ export function implicitConfigJson(resolved) {
4
+ if (resolved.source === "file")
5
+ return {};
6
+ const line = resolved.config.lines[0];
7
+ return {
8
+ configSource: resolved.source,
9
+ pattern: line.pattern,
10
+ };
11
+ }
12
+ /** One-time human notice that implicit semver defaults are in use. */
13
+ export function printImplicitConfigNotice(resolved, json) {
14
+ if (json || resolved.source === "file")
15
+ return;
16
+ const line = resolved.config.lines[0];
17
+ const kind = resolved.source === "inferred" ? "inferred" : "default";
18
+ info(`${color.dim("ℹ")} No ${color.bold(".tagsmith.json")} — using ${kind} semver defaults (pattern: ${line.pattern}).`);
19
+ info(` ${color.dim("Run `tagsmith init` to customize, or commit .tagsmith.json for the team.")}`);
20
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runInit } from "./init.js";
4
+ import { runList } from "./list.js";
5
+ import { runNext } from "./next.js";
6
+ import { runCreate } from "./create.js";
7
+ import { runGuide } from "./guide.js";
8
+ import { runCheck } from "./check.js";
9
+ import { printError } from "./ui.js";
10
+ const program = new Command();
11
+ program
12
+ .name("tagsmith")
13
+ .description("Define git tag specs, view tags, and generate the next git tag safely.")
14
+ .version("0.1.0");
15
+ program.addHelpText("beforeAll", "\n Tagsmith — define a tag spec, then safely compute and create git tags.\n" +
16
+ " First time here? Run `tagsmith init`, or `tagsmith guide` for a walkthrough.\n");
17
+ program.addHelpText("after", `
18
+ Examples:
19
+ $ tagsmith init Define the tag spec (interactive)
20
+ $ tagsmith guide Step-by-step walkthrough
21
+ $ tagsmith list Inspect existing tags
22
+ $ tagsmith check v1.2.3 Validate a tag against the spec
23
+ $ tagsmith next --level minor Preview the next tag
24
+ $ tagsmith next --tag release Compute next on a named tag line
25
+ $ tagsmith create --level minor --push Create and push the next tag
26
+ `);
27
+ program
28
+ .command("init")
29
+ .description("Create a .tagsmith.json tag spec for this repo")
30
+ .option("--pattern <pattern>", "tag pattern, must contain {version}")
31
+ .option("--model <type>", "version model: semver | calver | build")
32
+ .option("--initial-version <version>", "initial version")
33
+ .option("--push", "push tags by default on create")
34
+ .option("--force", "overwrite an existing config")
35
+ .option("-y, --yes", "non-interactive; use flags/defaults")
36
+ .action(async (opts) => {
37
+ process.exitCode = await runInit(process.cwd(), opts);
38
+ });
39
+ program
40
+ .command("guide")
41
+ .description("Interactive walkthrough: init → list → next → create")
42
+ .action(async () => {
43
+ process.exitCode = await runGuide(process.cwd());
44
+ });
45
+ program
46
+ .command("list")
47
+ .alias("ls")
48
+ .description("List git tags, sorted and validated against the spec")
49
+ .option("--json", "output JSON")
50
+ .option("-t, --tag <name>", "list only the named tag line")
51
+ .option("--all", "list every tag line plus unassigned tags")
52
+ .action(async (opts) => {
53
+ process.exitCode = await runList(process.cwd(), opts);
54
+ });
55
+ program
56
+ .command("next")
57
+ .description("Compute the next tag without creating it")
58
+ .option("-l, --level <level>", "bump level: major | minor | patch | prerelease | auto", "patch")
59
+ .option("--json", "output JSON")
60
+ .option("-t, --tag <name>", "operate on the named tag line (default: the config's default line)")
61
+ .action(async (opts) => {
62
+ process.exitCode = await runNext(process.cwd(), opts);
63
+ });
64
+ program
65
+ .command("check [tags...]")
66
+ .description("Validate tags against the spec; with no args, lint all repo tags")
67
+ .option("--json", "output JSON")
68
+ .option("-t, --tag <name>", "validate only against the named tag line")
69
+ .action(async (tags, opts) => {
70
+ process.exitCode = await runCheck(process.cwd(), tags, opts);
71
+ });
72
+ const createCommand = program
73
+ .command("create")
74
+ .description("Create the next (or an explicit) git tag")
75
+ .option("-l, --level <level>", "bump level: major | minor | patch | prerelease | auto", "patch")
76
+ .option("--set-version <version>", "create an explicit version instead of bumping")
77
+ .option("-m, --message <message>", "annotate the tag with a message")
78
+ .option("--push", "push the tag after creating")
79
+ .option("--dry-run", "preview without creating")
80
+ .option("--allow-out-of-order", "permit a version not greater than latest")
81
+ .option("-t, --tag <name>", "operate on the named tag line (default: the config's default line)")
82
+ .action(async (opts) => {
83
+ process.exitCode = await runCreate(process.cwd(), opts);
84
+ });
85
+ createCommand.addHelpText("after", `
86
+ Examples:
87
+ $ tagsmith create Create the next patch tag
88
+ $ tagsmith create -l minor -m "..." Create an annotated minor tag
89
+ $ tagsmith create --set-version 2.0.0 Create an explicit version
90
+ $ tagsmith create --dry-run Preview without creating
91
+ $ tagsmith create --tag release Create the next tag on a named tag line
92
+ `);
93
+ program.parseAsync(process.argv).catch((err) => {
94
+ printError(err);
95
+ process.exitCode = 1;
96
+ });
@@ -0,0 +1,11 @@
1
+ export interface InitFlags {
2
+ pattern?: string;
3
+ model?: string;
4
+ initialVersion?: string;
5
+ push?: boolean;
6
+ force?: boolean;
7
+ yes?: boolean;
8
+ hints?: boolean;
9
+ }
10
+ /** Run the `init` command. Returns the process exit code. */
11
+ export declare function runInit(cwd: string, flags: InitFlags): Promise<number>;
@@ -0,0 +1,127 @@
1
+ import * as p from "@clack/prompts";
2
+ import { CONFIG_FILENAME, configExists, writeConfig, } from "../core/config.js";
3
+ import { printError, success } from "./ui.js";
4
+ import { printNextStepsAfterInit } from "./guidance.js";
5
+ /** Run the `init` command. Returns the process exit code. */
6
+ export async function runInit(cwd, flags) {
7
+ if ((await configExists(cwd)) && flags.force !== true) {
8
+ printError(`${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
9
+ return 1;
10
+ }
11
+ const config = flags.yes
12
+ ? buildFromFlags(flags)
13
+ : await promptForConfig(flags);
14
+ if (config === null) {
15
+ printError("Aborted.");
16
+ return 1;
17
+ }
18
+ await writeConfig(cwd, config);
19
+ success(`Wrote ${CONFIG_FILENAME}`);
20
+ if (flags.hints !== false)
21
+ printNextStepsAfterInit({});
22
+ return 0;
23
+ }
24
+ function buildFromFlags(flags) {
25
+ const modelType = (flags.model ?? "semver");
26
+ return {
27
+ lines: [
28
+ {
29
+ name: "default",
30
+ pattern: flags.pattern ?? "v{version}",
31
+ model: defaultModel(modelType),
32
+ initialVersion: flags.initialVersion ?? defaultInitial(modelType),
33
+ push: flags.push ?? false,
34
+ },
35
+ ],
36
+ default: "default",
37
+ };
38
+ }
39
+ async function promptForConfig(flags) {
40
+ p.intro("tagsmith init");
41
+ const pattern = await p.text({
42
+ message: "Tag pattern (must contain {version})",
43
+ initialValue: flags.pattern ?? "v{version}",
44
+ validate: (v) => v.includes("{version}") ? undefined : "Must contain {version}",
45
+ });
46
+ if (p.isCancel(pattern))
47
+ return null;
48
+ const modelType = (await p.select({
49
+ message: "Version model",
50
+ initialValue: (flags.model ?? "semver"),
51
+ options: [
52
+ { value: "semver", label: "SemVer (1.2.3)" },
53
+ { value: "calver", label: "CalVer (2026.06.0)" },
54
+ { value: "build", label: "Build number (42)" },
55
+ ],
56
+ }));
57
+ if (p.isCancel(modelType))
58
+ return null;
59
+ const model = await promptModelDetails(modelType);
60
+ if (model === null)
61
+ return null;
62
+ const initialVersion = await p.text({
63
+ message: "Initial version (used when no tag exists yet)",
64
+ initialValue: flags.initialVersion ?? defaultInitial(modelType),
65
+ });
66
+ if (p.isCancel(initialVersion))
67
+ return null;
68
+ const push = await p.confirm({
69
+ message: "Push tags by default on create?",
70
+ initialValue: flags.push ?? false,
71
+ });
72
+ if (p.isCancel(push))
73
+ return null;
74
+ p.outro("Configured.");
75
+ return {
76
+ lines: [{ name: "default", pattern, model, initialVersion, push }],
77
+ default: "default",
78
+ };
79
+ }
80
+ async function promptModelDetails(type) {
81
+ if (type === "semver") {
82
+ const allowPrerelease = await p.confirm({
83
+ message: "Allow prerelease versions (e.g. 1.2.3-rc.1)?",
84
+ initialValue: true,
85
+ });
86
+ if (p.isCancel(allowPrerelease))
87
+ return null;
88
+ return { type: "semver", allowPrerelease };
89
+ }
90
+ if (type === "calver") {
91
+ const format = await p.text({
92
+ message: "CalVer format (tokens: YYYY YY MM DD MICRO)",
93
+ initialValue: "YYYY.MM.MICRO",
94
+ });
95
+ if (p.isCancel(format))
96
+ return null;
97
+ return { type: "calver", format };
98
+ }
99
+ const padding = await p.text({
100
+ message: "Zero-pad build number to width (0 = none)",
101
+ initialValue: "0",
102
+ validate: (v) => (/^\d+$/.test(v) ? undefined : "Must be a number"),
103
+ });
104
+ if (p.isCancel(padding))
105
+ return null;
106
+ return { type: "build", padding: Number(padding) };
107
+ }
108
+ function defaultModel(type) {
109
+ switch (type) {
110
+ case "semver":
111
+ return { type: "semver", allowPrerelease: true };
112
+ case "calver":
113
+ return { type: "calver", format: "YYYY.MM.MICRO" };
114
+ case "build":
115
+ return { type: "build", padding: 0 };
116
+ }
117
+ }
118
+ function defaultInitial(type) {
119
+ switch (type) {
120
+ case "semver":
121
+ return "0.1.0";
122
+ case "calver":
123
+ return "2026.06.0";
124
+ case "build":
125
+ return "1";
126
+ }
127
+ }
@@ -0,0 +1,6 @@
1
+ export interface ListFlags {
2
+ json?: boolean;
3
+ tag?: string;
4
+ all?: boolean;
5
+ }
6
+ export declare function runList(cwd: string, flags: ListFlags): Promise<number>;
@@ -0,0 +1,124 @@
1
+ import { compilePattern } from "../core/pattern.js";
2
+ import { createModel } from "../core/models/index.js";
3
+ import { analyzeTags } from "../core/analyze.js";
4
+ import { assignTagsToLines, selectLine } from "../core/lines.js";
5
+ import { ensureRepo, listTags } from "../git/git.js";
6
+ import { color, info, printError, warn } from "./ui.js";
7
+ import { resolveConfig } from "./resolve-config.js";
8
+ import { implicitConfigJson, printImplicitConfigNotice } from "./implicit.js";
9
+ /** Render the conforming + anomaly listing for an analysis (human-readable). */
10
+ function printAnalysisBody(analysis) {
11
+ info(color.bold("Conforming tags (newest first):"));
12
+ if (analysis.conforming.length === 0) {
13
+ info(" (none)");
14
+ }
15
+ analysis.conforming.forEach((t, i) => {
16
+ const marker = i === 0 ? color.green(" ← latest") : "";
17
+ info(` ${color.cyan(t.raw)}${marker}`);
18
+ });
19
+ if (analysis.anomalies.length > 0) {
20
+ info("");
21
+ warn(`${analysis.anomalies.length} non-conforming tag(s):`);
22
+ for (const t of analysis.anomalies) {
23
+ info(` ${color.yellow(t.raw)} ${color.dim(`(${t.anomaly})`)}`);
24
+ }
25
+ }
26
+ }
27
+ /** Serialise an analysis to the shared `--json` shape. */
28
+ function analysisToJson(analysis) {
29
+ return {
30
+ conforming: analysis.conforming.map((t) => ({
31
+ tag: t.raw,
32
+ version: t.versionString,
33
+ })),
34
+ anomalies: analysis.anomalies.map((t) => ({
35
+ tag: t.raw,
36
+ reason: t.anomaly,
37
+ })),
38
+ latest: analysis.latest?.raw ?? null,
39
+ };
40
+ }
41
+ /** Print the analysis for a single named line (human-readable, with header). */
42
+ function printLineSection(lineName, analysis) {
43
+ info(color.bold(`\n── Line: ${lineName} ──`));
44
+ if (analysis.conforming.length === 0 && analysis.anomalies.length === 0) {
45
+ info(" (no tags)");
46
+ return;
47
+ }
48
+ printAnalysisBody(analysis);
49
+ }
50
+ /** Print the orphan tags section (human-readable). */
51
+ function printOrphans(orphans) {
52
+ info(color.bold("\n── Unassigned / orphan tags ──"));
53
+ for (const t of orphans) {
54
+ info(` ${color.yellow(t)}`);
55
+ }
56
+ }
57
+ export async function runList(cwd, flags) {
58
+ try {
59
+ const resolved = await resolveConfig(cwd);
60
+ const { config } = resolved;
61
+ await ensureRepo({ cwd });
62
+ const allTags = await listTags({ cwd });
63
+ const assignment = assignTagsToLines(allTags, config.lines);
64
+ if (flags.all && flags.tag) {
65
+ printError("--all and --tag are mutually exclusive.");
66
+ return 1;
67
+ }
68
+ if (flags.all) {
69
+ if (flags.json) {
70
+ const lines = config.lines.map((line) => {
71
+ const model = createModel(line.model);
72
+ const pattern = compilePattern(line.pattern);
73
+ const lineTags = assignment.byLine.get(line.name) ?? [];
74
+ const analysis = analyzeTags(lineTags, pattern, model);
75
+ return { line: line.name, ...analysisToJson(analysis) };
76
+ });
77
+ info(JSON.stringify({
78
+ lines,
79
+ orphans: assignment.orphans,
80
+ ...implicitConfigJson(resolved),
81
+ }, null, 2));
82
+ return 0;
83
+ }
84
+ // Human-readable --all output
85
+ for (const line of config.lines) {
86
+ const model = createModel(line.model);
87
+ const pattern = compilePattern(line.pattern);
88
+ const lineTags = assignment.byLine.get(line.name) ?? [];
89
+ const analysis = analyzeTags(lineTags, pattern, model);
90
+ printLineSection(line.name, analysis);
91
+ }
92
+ if (assignment.orphans.length > 0) {
93
+ printOrphans(assignment.orphans);
94
+ }
95
+ return 0;
96
+ }
97
+ // Single-line path: default or --tag <name>
98
+ const line = selectLine(config, flags.tag);
99
+ const model = createModel(line.model);
100
+ const pattern = compilePattern(line.pattern);
101
+ const lineTags = assignment.byLine.get(line.name) ?? [];
102
+ // Single-line config: feed allTags so non-matching tags appear as pattern-mismatch
103
+ // anomalies — preserves backward-compatible behaviour (allTags === lineTags + orphans).
104
+ // Multi-line config: feed only this line's own bucket (spec 5.3); orphans belong to
105
+ // the `list --all` view and must NOT pollute a single-line analysis.
106
+ const tagsForAnalysis = config.lines.length === 1 ? allTags : lineTags;
107
+ const analysis = analyzeTags(tagsForAnalysis, pattern, model);
108
+ if (flags.json) {
109
+ info(JSON.stringify({ line: line.name, ...analysisToJson(analysis), ...implicitConfigJson(resolved) }, null, 2));
110
+ return 0;
111
+ }
112
+ printImplicitConfigNotice(resolved, flags.json);
113
+ if (analysis.conforming.length === 0 && analysis.anomalies.length === 0) {
114
+ info("No tags found.");
115
+ return 0;
116
+ }
117
+ printAnalysisBody(analysis);
118
+ return 0;
119
+ }
120
+ catch (err) {
121
+ printError(err);
122
+ return 1;
123
+ }
124
+ }
@@ -0,0 +1,7 @@
1
+ export interface NextFlags {
2
+ level?: string;
3
+ json?: boolean;
4
+ hints?: boolean;
5
+ tag?: string;
6
+ }
7
+ export declare function runNext(cwd: string, flags: NextFlags): Promise<number>;
@@ -0,0 +1,57 @@
1
+ import { createModel } from "../core/models/index.js";
2
+ import { planNext } from "../core/plan.js";
3
+ import { assignTagsToLines, selectLine } from "../core/lines.js";
4
+ import { ensureRepo, listTags } from "../git/git.js";
5
+ import { color, info, printError, warn } from "./ui.js";
6
+ import { printNextStepsAfterNext } from "./guidance.js";
7
+ import { resolveConfig } from "./resolve-config.js";
8
+ import { implicitConfigJson, printImplicitConfigNotice } from "./implicit.js";
9
+ const LEVELS = ["major", "minor", "patch", "prerelease", "auto"];
10
+ export async function runNext(cwd, flags) {
11
+ try {
12
+ const level = resolveLevel(flags.level);
13
+ const resolved = await resolveConfig(cwd);
14
+ const { config } = resolved;
15
+ await ensureRepo({ cwd });
16
+ const line = selectLine(config, flags.tag);
17
+ const model = createModel(line.model);
18
+ const allTags = await listTags({ cwd });
19
+ const lineTags = assignTagsToLines(allTags, config.lines).byLine.get(line.name) ?? [];
20
+ const plan = planNext(line, model, lineTags, level);
21
+ if (plan.fresh && plan.analysis.anomalies.length > 0 && !flags.json) {
22
+ warn(`${plan.analysis.anomalies.length} non-conforming tag(s) ignored; treating repo as having no prior version. Run \`tagsmith list\` to inspect.`);
23
+ }
24
+ if (flags.json) {
25
+ info(JSON.stringify({
26
+ tag: plan.tag,
27
+ version: plan.version,
28
+ fromVersion: plan.fromVersion,
29
+ fresh: plan.fresh,
30
+ line: line.name,
31
+ ...implicitConfigJson(resolved),
32
+ }, null, 2));
33
+ return 0;
34
+ }
35
+ printImplicitConfigNotice(resolved, flags.json);
36
+ if (plan.fresh) {
37
+ info(`${color.cyan(plan.tag)} ${color.dim("(initial — no prior tag)")}`);
38
+ }
39
+ else {
40
+ info(`${color.cyan(plan.tag)} ${color.dim(`(from ${plan.fromVersion})`)}`);
41
+ }
42
+ if (flags.hints !== false)
43
+ printNextStepsAfterNext({ level, json: flags.json });
44
+ return 0;
45
+ }
46
+ catch (err) {
47
+ printError(err);
48
+ return 1;
49
+ }
50
+ }
51
+ function resolveLevel(raw) {
52
+ if (raw === undefined)
53
+ return "patch";
54
+ if (LEVELS.includes(raw))
55
+ return raw;
56
+ throw new Error(`Invalid level "${raw}". Expected one of: ${LEVELS.join(", ")}`);
57
+ }
@@ -0,0 +1,6 @@
1
+ import { type ResolvedConfig } from "../core/config.js";
2
+ /**
3
+ * Load `.tagsmith.json` when present; otherwise build semver defaults from
4
+ * `hintTags` or the repo's existing tags.
5
+ */
6
+ export declare function resolveConfig(cwd: string, hintTags?: readonly string[]): Promise<ResolvedConfig>;
@@ -0,0 +1,16 @@
1
+ import { buildImplicitConfig, configExists, loadConfig, } from "../core/config.js";
2
+ import { ensureRepo, listTags } from "../git/git.js";
3
+ /**
4
+ * Load `.tagsmith.json` when present; otherwise build semver defaults from
5
+ * `hintTags` or the repo's existing tags.
6
+ */
7
+ export async function resolveConfig(cwd, hintTags) {
8
+ if (await configExists(cwd)) {
9
+ return { config: await loadConfig(cwd), source: "file" };
10
+ }
11
+ if (hintTags !== undefined && hintTags.length > 0) {
12
+ return buildImplicitConfig(hintTags);
13
+ }
14
+ await ensureRepo({ cwd });
15
+ return buildImplicitConfig(await listTags({ cwd }));
16
+ }
@@ -0,0 +1,8 @@
1
+ /** Print an error to stderr in a consistent, friendly format. */
2
+ export declare function printError(err: unknown): void;
3
+ export declare function info(msg: string): void;
4
+ export declare function success(msg: string): void;
5
+ export declare function warn(msg: string): void;
6
+ export declare const color: import("picocolors/types").Colors & {
7
+ createColors: (enabled?: boolean) => import("picocolors/types").Colors;
8
+ };
package/dist/cli/ui.js ADDED
@@ -0,0 +1,16 @@
1
+ import pc from "picocolors";
2
+ /** Print an error to stderr in a consistent, friendly format. */
3
+ export function printError(err) {
4
+ const message = err instanceof Error ? err.message : String(err);
5
+ process.stderr.write(`${pc.red("✖")} ${message}\n`);
6
+ }
7
+ export function info(msg) {
8
+ process.stdout.write(`${msg}\n`);
9
+ }
10
+ export function success(msg) {
11
+ process.stdout.write(`${pc.green("✔")} ${msg}\n`);
12
+ }
13
+ export function warn(msg) {
14
+ process.stdout.write(`${pc.yellow("!")} ${msg}\n`);
15
+ }
16
+ export const color = pc;
@@ -0,0 +1,21 @@
1
+ import type { CompiledPattern } from "./pattern.js";
2
+ import type { ParsedTag, VersionModel } from "../types.js";
3
+ export interface AnalyzedTag extends ParsedTag {
4
+ /** True when the tag matches the pattern and the version parses. */
5
+ conforming: boolean;
6
+ }
7
+ export interface Analysis {
8
+ /** Conforming tags sorted newest → oldest by version. */
9
+ conforming: AnalyzedTag[];
10
+ /** Tags that failed pattern or version parsing, in input order. */
11
+ anomalies: AnalyzedTag[];
12
+ /** The highest conforming version, or null when none exist. */
13
+ latest: AnalyzedTag | null;
14
+ }
15
+ /**
16
+ * Classify a list of raw git tag names against the configured pattern + model,
17
+ * sort the conforming ones by semantic version (newest first), and surface
18
+ * format / duplicate anomalies.
19
+ */
20
+ export declare function analyzeTags(tags: readonly string[], pattern: CompiledPattern, model: VersionModel): Analysis;
21
+ export declare function classify(raw: string, pattern: CompiledPattern, model: VersionModel): AnalyzedTag;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Classify a list of raw git tag names against the configured pattern + model,
3
+ * sort the conforming ones by semantic version (newest first), and surface
4
+ * format / duplicate anomalies.
5
+ */
6
+ export function analyzeTags(tags, pattern, model) {
7
+ const parsed = tags.map((raw) => classify(raw, pattern, model));
8
+ // Detect duplicate versions among otherwise-conforming tags.
9
+ const seen = new Map();
10
+ for (const tag of parsed) {
11
+ if (tag.version === null || tag.versionString === null)
12
+ continue;
13
+ const key = model.format(tag.version);
14
+ const prior = seen.get(key);
15
+ if (prior === undefined) {
16
+ seen.set(key, tag);
17
+ }
18
+ else {
19
+ tag.anomaly = "duplicate-version";
20
+ tag.conforming = false;
21
+ }
22
+ }
23
+ const conforming = parsed
24
+ .filter((t) => t.conforming)
25
+ .sort((a, b) => model.compare(b.version, a.version));
26
+ const anomalies = parsed.filter((t) => !t.conforming);
27
+ return {
28
+ conforming,
29
+ anomalies,
30
+ latest: conforming[0] ?? null,
31
+ };
32
+ }
33
+ export function classify(raw, pattern, model) {
34
+ const versionString = pattern.extract(raw);
35
+ if (versionString === null) {
36
+ return {
37
+ raw,
38
+ versionString: null,
39
+ version: null,
40
+ anomaly: "pattern-mismatch",
41
+ conforming: false,
42
+ };
43
+ }
44
+ const version = model.parse(versionString);
45
+ if (version === null) {
46
+ return {
47
+ raw,
48
+ versionString,
49
+ version: null,
50
+ anomaly: "unparseable-version",
51
+ conforming: false,
52
+ };
53
+ }
54
+ return { raw, versionString, version, anomaly: null, conforming: true };
55
+ }
@@ -0,0 +1,15 @@
1
+ import type { CompiledPattern } from "./pattern.js";
2
+ import type { TagAnomaly, VersionModel } from "../types.js";
3
+ export interface TagCheck {
4
+ tag: string;
5
+ ok: boolean;
6
+ anomaly: TagAnomaly | null;
7
+ }
8
+ export interface CheckResult {
9
+ ok: boolean;
10
+ checks: TagCheck[];
11
+ }
12
+ /**
13
+ * 驗證候選 tag 是否符合 pattern + model。純函式,無 IO。
14
+ */
15
+ export declare function checkTags(pattern: CompiledPattern, model: VersionModel, candidates: readonly string[], existing: readonly string[]): CheckResult;
@@ -0,0 +1,27 @@
1
+ import { classify } from "./analyze.js";
2
+ /**
3
+ * 驗證候選 tag 是否符合 pattern + model。純函式,無 IO。
4
+ */
5
+ export function checkTags(pattern, model, candidates, existing) {
6
+ // 既有 conforming 版本的鍵集合。
7
+ const seen = new Set();
8
+ for (const raw of existing) {
9
+ const c = classify(raw, pattern, model);
10
+ if (c.conforming && c.version !== null) {
11
+ seen.add(model.format(c.version));
12
+ }
13
+ }
14
+ const checks = candidates.map((tag) => {
15
+ const c = classify(tag, pattern, model);
16
+ if (!c.conforming) {
17
+ return { tag, ok: false, anomaly: c.anomaly };
18
+ }
19
+ const key = model.format(c.version);
20
+ if (seen.has(key)) {
21
+ return { tag, ok: false, anomaly: "duplicate-version" };
22
+ }
23
+ seen.add(key);
24
+ return { tag, ok: true, anomaly: null };
25
+ });
26
+ return { ok: checks.every((c) => c.ok), checks };
27
+ }
@@ -0,0 +1,21 @@
1
+ import type { TagsmithConfig } from "../types.js";
2
+ export declare const CONFIG_FILENAME = ".tagsmith.json";
3
+ export declare class ConfigError extends Error {
4
+ }
5
+ /** Thrown by loadConfig when no config file exists (vs. a malformed one). */
6
+ export declare class MissingConfigError extends ConfigError {
7
+ }
8
+ export type ConfigSource = "file" | "inferred" | "default";
9
+ export interface ResolvedConfig {
10
+ config: TagsmithConfig;
11
+ source: ConfigSource;
12
+ }
13
+ /** Build semver defaults, inferring pattern from existing tag names when possible. */
14
+ export declare function buildImplicitConfig(tags: readonly string[]): ResolvedConfig;
15
+ /** Parse, normalise and validate a raw config. Throws ConfigError on failure. */
16
+ export declare function parseConfig(raw: unknown): TagsmithConfig;
17
+ export declare function configPath(cwd: string): string;
18
+ export declare function configExists(cwd: string): Promise<boolean>;
19
+ /** Load and validate the config from `cwd`. Throws ConfigError when missing. */
20
+ export declare function loadConfig(cwd: string): Promise<TagsmithConfig>;
21
+ export declare function writeConfig(cwd: string, config: TagsmithConfig): Promise<void>;