@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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.js +83 -0
- package/dist/cli/create.d.ts +10 -0
- package/dist/cli/create.js +75 -0
- package/dist/cli/guidance.d.ts +14 -0
- package/dist/cli/guidance.js +43 -0
- package/dist/cli/guide.d.ts +17 -0
- package/dist/cli/guide.js +61 -0
- package/dist/cli/implicit.d.ts +5 -0
- package/dist/cli/implicit.js +20 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +96 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.js +127 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +124 -0
- package/dist/cli/next.d.ts +7 -0
- package/dist/cli/next.js +57 -0
- package/dist/cli/resolve-config.d.ts +6 -0
- package/dist/cli/resolve-config.js +16 -0
- package/dist/cli/ui.d.ts +8 -0
- package/dist/cli/ui.js +16 -0
- package/dist/core/analyze.d.ts +21 -0
- package/dist/core/analyze.js +55 -0
- package/dist/core/check.d.ts +15 -0
- package/dist/core/check.js +27 -0
- package/dist/core/config.d.ts +21 -0
- package/dist/core/config.js +152 -0
- package/dist/core/defaults.d.ts +6 -0
- package/dist/core/defaults.js +16 -0
- package/dist/core/infer.d.ts +6 -0
- package/dist/core/infer.js +35 -0
- package/dist/core/lines.d.ts +13 -0
- package/dist/core/lines.js +27 -0
- package/dist/core/models/build.d.ts +13 -0
- package/dist/core/models/build.js +33 -0
- package/dist/core/models/calver.d.ts +19 -0
- package/dist/core/models/calver.js +166 -0
- package/dist/core/models/index.d.ts +10 -0
- package/dist/core/models/index.js +21 -0
- package/dist/core/models/semver.d.ts +13 -0
- package/dist/core/models/semver.js +57 -0
- package/dist/core/pattern.d.ts +15 -0
- package/dist/core/pattern.js +29 -0
- package/dist/core/plan.d.ts +33 -0
- package/dist/core/plan.js +59 -0
- package/dist/git/git.d.ts +23 -0
- package/dist/git/git.js +47 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
- 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,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>;
|
package/dist/cli/init.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/list.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/next.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/ui.d.ts
ADDED
|
@@ -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>;
|