@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 carl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # Tagsmith
2
+
3
+ 定義專案的 git tag 規格、檢視現有 tag,並安全地產生下一個 git tag——避免順序錯亂或格式不一致。
4
+
5
+ 支援 **SemVer**、**CalVer** 與 **build number** 三種版本模型,tag 樣式可自訂(例如 `v{version}`、`release/{version}`)。
6
+
7
+ - 🏷️ **規格化** — 用一個 `.tagsmith.json` 定義全專案的 tag 樣式與版本模型
8
+ - 🔍 **可檢視** — 依語義排序列出 tag,標示格式 / 順序 / 重複異常
9
+ - 🛡️ **防呆** — 建立前驗證格式、版本可解析、嚴格遞增、tag 不重複
10
+ - 🧩 **可擴充** — 版本模型走介面抽象,新增不動核心邏輯
11
+
12
+ ## 安裝
13
+
14
+ ```bash
15
+ npm install -g tagsmith
16
+ # 或免安裝直接執行
17
+ npx tagsmith <command>
18
+ ```
19
+
20
+ 需求:Node.js ≥ 18、git。
21
+
22
+ ## 快速開始
23
+
24
+ ```bash
25
+ # 1. 在 repo 內定義 tag 規格(互動式)
26
+ tagsmith init
27
+
28
+ # 不熟指令?走一次互動式導覽
29
+ tagsmith guide
30
+
31
+ # 2. 檢視現有 tag(依語義排序、標示異常)
32
+ tagsmith list
33
+
34
+ # 3. 預覽下一個 tag(不建立)
35
+ tagsmith next --level minor
36
+
37
+ # 4. 建立 tag(自動驗證格式與順序)
38
+ tagsmith create --level minor -m "Release 1.2.0" --push
39
+ ```
40
+
41
+ ## 設定檔 `.tagsmith.json`
42
+
43
+ `tagsmith init` 會在 repo 根目錄產生設定檔。
44
+
45
+ ### 多條 tag 線
46
+
47
+ 一份設定檔可定義多條獨立的 tag 線,各線有自己的 pattern 與版本模型,彼此獨立遞增:
48
+
49
+ ```json
50
+ {
51
+ "tags": [
52
+ {
53
+ "name": "app",
54
+ "pattern": "v{version}",
55
+ "model": { "type": "semver", "allowPrerelease": true },
56
+ "initialVersion": "0.1.0",
57
+ "push": false
58
+ },
59
+ {
60
+ "name": "release",
61
+ "pattern": "release/{version}",
62
+ "model": { "type": "calver", "format": "YYYY.MM.MICRO" },
63
+ "initialVersion": "2026.06.0",
64
+ "push": true
65
+ }
66
+ ],
67
+ "default": "app"
68
+ }
69
+ ```
70
+
71
+ 每條線的欄位:
72
+
73
+ | 欄位 | 必填 | 說明 |
74
+ |------|:---:|------|
75
+ | `name` | ✓ | 線名,全陣列唯一,供 `--tag` 指定 |
76
+ | `pattern` | ✓ | tag 樣式,**必含** `{version}` 佔位符。例:`v{version}`、`release/{version}` |
77
+ | `model` | ✓ | 版本模型物件(見下) |
78
+ | `initialVersion` | ✓ | 無既有合規 tag 時的起點 |
79
+ | `push` | | `create` 是否預設 push(預設 `false`) |
80
+
81
+ 頂層選填欄位:
82
+
83
+ | 欄位 | 說明 |
84
+ |------|------|
85
+ | `default` | 預設操作線名;省略時取 `tags[0].name` |
86
+
87
+ ### 舊格式(仍相容)
88
+
89
+ 既有的單線扁平格式無需修改,仍可正常載入:
90
+
91
+ ```json
92
+ {
93
+ "pattern": "v{version}",
94
+ "model": { "type": "semver", "allowPrerelease": true },
95
+ "initialVersion": "0.1.0",
96
+ "push": false
97
+ }
98
+ ```
99
+
100
+ Tagsmith 載入時會自動將其視為一條名為 `default` 的單線設定;現有使用者**零修改**即可繼續使用。
101
+
102
+ 可在檔案加上 `"$schema": "./node_modules/tagsmith/schema.json"` 取得編輯器補全與驗證。
103
+
104
+ ### 三種版本模型
105
+
106
+ | 模型 | 範例 | 專屬設定 | 遞增規則 |
107
+ |------|------|----------|----------|
108
+ | `semver` | `1.2.3`、`1.2.3-rc.1` | `allowPrerelease`(預設 `true`) | `--level major/minor/patch/prerelease` |
109
+ | `calver` | `2026.06.0` | `format`(token:`YYYY YY MM DD MICRO`) | 滾動到當天;同日遞增 `MICRO` |
110
+ | `build` | `0042` | `padding`(補零位數,預設 `0`) | 單調 +1 |
111
+
112
+ <details>
113
+ <summary>各模型設定範例(舊格式)</summary>
114
+
115
+ **SemVer**(`v1.2.3`)
116
+
117
+ ```json
118
+ {
119
+ "pattern": "v{version}",
120
+ "model": { "type": "semver", "allowPrerelease": true },
121
+ "initialVersion": "0.1.0",
122
+ "push": false
123
+ }
124
+ ```
125
+
126
+ **CalVer**(`release/2026.06.0`,token 之間需有分隔字元或固定寬度)
127
+
128
+ ```json
129
+ {
130
+ "pattern": "release/{version}",
131
+ "model": { "type": "calver", "format": "YYYY.MM.MICRO" },
132
+ "initialVersion": "2026.06.0",
133
+ "push": false
134
+ }
135
+ ```
136
+
137
+ **Build number**(`build-0042`)
138
+
139
+ ```json
140
+ {
141
+ "pattern": "build-{version}",
142
+ "model": { "type": "build", "padding": 4 },
143
+ "initialVersion": "1",
144
+ "push": false
145
+ }
146
+ ```
147
+
148
+ </details>
149
+
150
+ > **CalVer `format` 注意**:可解析的數字寬度固定——`YYYY`=4 碼、`YY`/`MM`/`DD`=2 碼、
151
+ > `MICRO` 不限。未補零(如 `2026.6.0`)或含前導零的 `MICRO`(如 `...007`)會被視為
152
+ > **非合規**,以避免假冒或重複正規 tag。
153
+
154
+ ## 指令
155
+
156
+ ### `tagsmith init`
157
+ 互動式產生 `.tagsmith.json`。
158
+
159
+ | 旗標 | 說明 |
160
+ |------|------|
161
+ | `--pattern <pattern>` | tag 樣式 |
162
+ | `--model <type>` | `semver` \| `calver` \| `build` |
163
+ | `--initial-version <version>` | 初始版本 |
164
+ | `--push` | 預設 push |
165
+ | `--force` | 覆寫既有設定檔 |
166
+ | `-y, --yes` | 非互動,使用旗標 / 預設值 |
167
+
168
+ ### `tagsmith list` (`ls`)
169
+ 列出 git tag,依規格解析後由新到舊排序,並標示異常 tag:
170
+ **不符樣式**(`pattern-mismatch`)、**版本無法解析**(`unparseable-version`)、
171
+ **重複版本**(`duplicate-version`)。
172
+
173
+ 預設只列出 `default` 線的 tag;多線設定可用 `--tag` 指定線或 `--all` 一次列出所有線。
174
+
175
+ | 旗標 | 說明 |
176
+ |------|------|
177
+ | `--json` | 輸出結構化 JSON |
178
+ | `-t, --tag <name>` | 只列出指定線的 tag |
179
+ | `--all` | 列出每條線(各自分組)與無主 tag(Unassigned / orphan tags) |
180
+
181
+ ```jsonc
182
+ // tagsmith list --json(單線或指定 --tag)
183
+ {
184
+ "conforming": [
185
+ { "tag": "v1.2.0", "version": "1.2.0" },
186
+ { "tag": "v1.1.0", "version": "1.1.0" }
187
+ ],
188
+ "anomalies": [
189
+ { "tag": "nightly", "reason": "pattern-mismatch" }
190
+ ],
191
+ "latest": "v1.2.0"
192
+ }
193
+ ```
194
+
195
+ ```jsonc
196
+ // tagsmith list --all --json(多線)
197
+ {
198
+ "lines": [
199
+ {
200
+ "line": "app",
201
+ "conforming": [{ "tag": "v1.2.0", "version": "1.2.0" }],
202
+ "anomalies": [],
203
+ "latest": "v1.2.0"
204
+ },
205
+ {
206
+ "line": "release",
207
+ "conforming": [{ "tag": "release/2026.06.0", "version": "2026.06.0" }],
208
+ "anomalies": [],
209
+ "latest": "release/2026.06.0"
210
+ }
211
+ ],
212
+ "orphans": ["legacy-tag"]
213
+ }
214
+ ```
215
+
216
+ ### `tagsmith check`
217
+ 驗證指定 tag 是否符合規格;不帶參數時檢查 repo 內所有既有 tag。
218
+ 適合用於 CI 或 git hook(exit 0 = 全部合規,exit 1 = 發現異常)。
219
+
220
+ 多線設定下,每個 tag 會對照所有線進行比對,並在結果中回報歸屬線(或 `null` 表示無主)。
221
+ `--tag <name>` 可限定只對某條線驗證。
222
+
223
+ | 旗標 | 說明 |
224
+ |------|------|
225
+ | `--json` | 輸出結構化 JSON |
226
+ | `-t, --tag <name>` | 只對指定線驗證 |
227
+
228
+ ```jsonc
229
+ // tagsmith check v1.2.3 "release/2026.06.1" junk --json
230
+ {
231
+ "results": [
232
+ { "raw": "v1.2.3", "line": "app", "ok": true, "anomaly": null },
233
+ { "raw": "release/2026.06.1","line": "release", "ok": true, "anomaly": null },
234
+ { "raw": "junk", "line": null, "ok": false, "anomaly": "pattern-mismatch" }
235
+ ]
236
+ }
237
+ ```
238
+
239
+ ### `tagsmith next`
240
+ 計算並印出下一個 tag,**不**實際建立。保證結果嚴格大於目前最大合規版本;
241
+ 無既有合規 tag 時改用 `initialVersion`。
242
+
243
+ | 旗標 | 說明 |
244
+ |------|------|
245
+ | `-l, --level <level>` | `major` \| `minor` \| `patch` \| `prerelease` \| `auto`(預設 `patch`) |
246
+ | `--json` | 輸出 JSON |
247
+ | `-t, --tag <name>` | 操作指定線(預設:設定檔的 `default` 線) |
248
+
249
+ ```jsonc
250
+ // tagsmith next --level minor --json
251
+ { "tag": "v1.3.0", "version": "1.3.0", "fromVersion": "1.2.0", "fresh": false, "line": "app" }
252
+ ```
253
+
254
+ ### `tagsmith create`
255
+ 建立下一個(或以 `--set-version` 指定的)tag。建立前驗證:格式符合 pattern、
256
+ 版本可解析、嚴格遞增、tag 不重複。push 行為優先取命令列 `--push`,其次取該線設定的 `push`。
257
+
258
+ | 旗標 | 說明 |
259
+ |------|------|
260
+ | `-l, --level <level>` | 遞增等級(同 `next`) |
261
+ | `--set-version <version>` | 改用指定版本,而非自動遞增 |
262
+ | `-m, --message <message>` | 建立 annotated tag |
263
+ | `--push` | 建立後推送(覆寫該線的 `push` 設定) |
264
+ | `--dry-run` | 只預覽,不建立 |
265
+ | `--allow-out-of-order` | 允許版本不大於現有最大值 |
266
+ | `-t, --tag <name>` | 操作指定線(預設:設定檔的 `default` 線) |
267
+
268
+ ## 常見情境
269
+
270
+ ```bash
271
+ # 發佈 patch 並推送
272
+ tagsmith create --push
273
+
274
+ # 發佈帶 annotation 的 minor release
275
+ tagsmith create -l minor -m "新增登入 API"
276
+
277
+ # CI 中取得下一個 tag 字串
278
+ NEXT=$(tagsmith next --json | jq -r .tag)
279
+
280
+ # 補一個歷史版本(明知順序在後)
281
+ tagsmith create --set-version 1.0.5 --allow-out-of-order
282
+
283
+ # 先看會發生什麼,不動 repo
284
+ tagsmith create -l major --dry-run
285
+
286
+ # 多線:在 release 線建立下一個 tag
287
+ tagsmith create --tag release
288
+
289
+ # 多線:預覽 release 線的下一個 tag
290
+ tagsmith next --tag release --json
291
+
292
+ # 多線:一次檢視所有線的 tag 狀況(含無主 tag)
293
+ tagsmith list --all
294
+ ```
295
+
296
+ ## 搭配 husky 守 tag
297
+
298
+ 可用 git `pre-push` hook 在推送時自動驗證 tag,擋下不符規格者。
299
+ 詳見 [docs/husky-pre-push.md](docs/husky-pre-push.md)。
300
+
301
+ ## 結束代碼
302
+
303
+ | 代碼 | 意義 |
304
+ |:---:|------|
305
+ | `0` | 成功(含 `--dry-run`) |
306
+ | `1` | 失敗:缺設定檔、非 git repo、驗證未通過、git 指令錯誤等(訊息走 stderr) |
307
+
308
+ ## 設計
309
+
310
+ 三層架構,各自可獨立測試:
311
+
312
+ - `core/` — 純函式(pattern、版本模型、analyze、plan、config 驗證),不碰 IO,時鐘由外部注入。
313
+ - `git/` — `git` 指令薄封裝(`execFile`,陣列參數、無 shell)。
314
+ - `cli/` — commander 指令組裝與輸出。
315
+
316
+ 詳見 [設計文件](docs/superpowers/specs/2026-06-10-tagsmith-design.md)。
317
+
318
+ ## 開發
319
+
320
+ ```bash
321
+ npm install
322
+ npm test # 跑全部測試(vitest)
323
+ npm run coverage # 覆蓋率(門檻 80%)
324
+ npm run build # 編譯到 dist/
325
+ npm run dev -- <command> # 以 tsx 直接執行原始碼
326
+ ```
327
+
328
+ 貢獻流程、新增版本模型的步驟見 [CONTRIBUTING.md](CONTRIBUTING.md);
329
+ 版本紀錄見 [CHANGELOG.md](CHANGELOG.md)。
330
+
331
+ ## License
332
+
333
+ [MIT](LICENSE) © 2026 carl
@@ -0,0 +1,5 @@
1
+ export interface CheckFlags {
2
+ json?: boolean;
3
+ tag?: string;
4
+ }
5
+ export declare function runCheck(cwd: string, tags: string[], flags: CheckFlags): Promise<number>;
@@ -0,0 +1,83 @@
1
+ import { compilePattern } from "../core/pattern.js";
2
+ import { createModel } from "../core/models/index.js";
3
+ import { classify } from "../core/analyze.js";
4
+ import { selectLine } from "../core/lines.js";
5
+ import { ensureRepo, listTags } from "../git/git.js";
6
+ import { color, info, printError, success } from "./ui.js";
7
+ import { resolveConfig } from "./resolve-config.js";
8
+ import { implicitConfigJson, printImplicitConfigNotice } from "./implicit.js";
9
+ /**
10
+ * Emit check results to stdout (JSON) or stdout/stderr (human-readable).
11
+ * Returns 0 if all results are ok, 1 if any is not ok.
12
+ */
13
+ function emitCheck(results, resolved, json) {
14
+ const allOk = results.every((r) => r.ok);
15
+ if (json) {
16
+ info(JSON.stringify({ results, ...implicitConfigJson(resolved) }, null, 2));
17
+ return allOk ? 0 : 1;
18
+ }
19
+ printImplicitConfigNotice(resolved, json);
20
+ for (const r of results) {
21
+ if (r.ok) {
22
+ success(`${color.cyan(r.raw)} ${color.dim("ok")} ${color.dim(`(${r.line ?? "orphan"})`)}`);
23
+ }
24
+ else {
25
+ printError(`${r.raw} (${r.anomaly})`);
26
+ }
27
+ }
28
+ return allOk ? 0 : 1;
29
+ }
30
+ export async function runCheck(cwd, tags, flags) {
31
+ try {
32
+ const resolved = tags.length > 0
33
+ ? await resolveConfig(cwd, tags)
34
+ : await resolveConfig(cwd);
35
+ const { config } = resolved;
36
+ let targets;
37
+ if (tags.length > 0) {
38
+ targets = tags;
39
+ }
40
+ else {
41
+ await ensureRepo({ cwd });
42
+ targets = await listTags({ cwd });
43
+ }
44
+ if (flags.tag) {
45
+ const line = selectLine(config, flags.tag);
46
+ const pattern = compilePattern(line.pattern);
47
+ const model = createModel(line.model);
48
+ const results = targets.map((raw) => {
49
+ const c = classify(raw, pattern, model);
50
+ return {
51
+ raw,
52
+ line: c.conforming ? line.name : null,
53
+ ok: c.conforming,
54
+ anomaly: c.anomaly,
55
+ };
56
+ });
57
+ return emitCheck(results, resolved, flags.json);
58
+ }
59
+ const compiled = config.lines.map((l) => ({
60
+ line: l,
61
+ pattern: compilePattern(l.pattern),
62
+ model: createModel(l.model),
63
+ }));
64
+ const results = targets.map((raw) => {
65
+ const hit = compiled.find((c) => c.pattern.extract(raw) !== null);
66
+ if (!hit) {
67
+ return { raw, line: null, ok: false, anomaly: "pattern-mismatch" };
68
+ }
69
+ const c = classify(raw, hit.pattern, hit.model);
70
+ return {
71
+ raw,
72
+ line: hit.line.name,
73
+ ok: c.conforming,
74
+ anomaly: c.anomaly,
75
+ };
76
+ });
77
+ return emitCheck(results, resolved, flags.json);
78
+ }
79
+ catch (err) {
80
+ printError(err);
81
+ return 1;
82
+ }
83
+ }
@@ -0,0 +1,10 @@
1
+ export interface CreateFlags {
2
+ level?: string;
3
+ setVersion?: string;
4
+ message?: string;
5
+ push?: boolean;
6
+ dryRun?: boolean;
7
+ allowOutOfOrder?: boolean;
8
+ tag?: string;
9
+ }
10
+ export declare function runCreate(cwd: string, flags: CreateFlags): Promise<number>;
@@ -0,0 +1,75 @@
1
+ import { compilePattern } from "../core/pattern.js";
2
+ import { createModel } from "../core/models/index.js";
3
+ import { planNext, validateExplicit } from "../core/plan.js";
4
+ import { assignTagsToLines, selectLine } from "../core/lines.js";
5
+ import { createTag, ensureRepo, listTags, pushTag } from "../git/git.js";
6
+ import { color, printError, success, warn } from "./ui.js";
7
+ import { printNextStepsAfterCreate } from "./guidance.js";
8
+ import { resolveConfig } from "./resolve-config.js";
9
+ import { printImplicitConfigNotice } from "./implicit.js";
10
+ const LEVELS = ["major", "minor", "patch", "prerelease", "auto"];
11
+ export async function runCreate(cwd, flags) {
12
+ try {
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 pattern = compilePattern(line.pattern);
19
+ const allTags = await listTags({ cwd });
20
+ const lineTags = assignTagsToLines(allTags, config.lines).byLine.get(line.name) ?? [];
21
+ let tagName;
22
+ if (flags.setVersion !== undefined) {
23
+ const result = validateExplicit(line, model, flags.setVersion, lineTags, {
24
+ allowOutOfOrder: flags.allowOutOfOrder,
25
+ });
26
+ if (!result.ok) {
27
+ for (const e of result.errors)
28
+ printError(e);
29
+ return 1;
30
+ }
31
+ const parsed = model.parse(flags.setVersion);
32
+ tagName = pattern.render(model.format(parsed));
33
+ }
34
+ else {
35
+ const level = resolveLevel(flags.level);
36
+ const plan = planNext(line, model, lineTags, level);
37
+ if (plan.fresh && plan.analysis.anomalies.length > 0) {
38
+ warn(`${plan.analysis.anomalies.length} non-conforming tag(s) ignored; treating repo as having no prior version.`);
39
+ }
40
+ tagName = plan.tag;
41
+ }
42
+ if (lineTags.includes(tagName)) {
43
+ printError(`Tag "${tagName}" already exists.`);
44
+ return 1;
45
+ }
46
+ if (flags.dryRun) {
47
+ printImplicitConfigNotice(resolved);
48
+ warn(`[dry-run] would create ${color.cyan(tagName)}${flags.message ? " (annotated)" : ""}`);
49
+ if (flags.push ?? line.push)
50
+ warn(`[dry-run] would push ${tagName}`);
51
+ return 0;
52
+ }
53
+ printImplicitConfigNotice(resolved);
54
+ await createTag({ cwd, name: tagName, message: flags.message });
55
+ success(`Created tag ${color.cyan(tagName)}`);
56
+ const willPush = flags.push ?? line.push;
57
+ if (willPush) {
58
+ await pushTag({ cwd, name: tagName });
59
+ success(`Pushed ${tagName}`);
60
+ }
61
+ printNextStepsAfterCreate({ pushed: willPush, tag: tagName });
62
+ return 0;
63
+ }
64
+ catch (err) {
65
+ printError(err);
66
+ return 1;
67
+ }
68
+ }
69
+ function resolveLevel(raw) {
70
+ if (raw === undefined)
71
+ return "patch";
72
+ if (LEVELS.includes(raw))
73
+ return raw;
74
+ throw new Error(`Invalid level "${raw}". Expected one of: ${LEVELS.join(", ")}`);
75
+ }
@@ -0,0 +1,14 @@
1
+ interface JsonAware {
2
+ json?: boolean;
3
+ }
4
+ /** Shown when a command needs a config but none exists yet. */
5
+ export declare function printFirstRunHint(opts?: JsonAware): void;
6
+ export declare function printNextStepsAfterInit(opts?: JsonAware): void;
7
+ export declare function printNextStepsAfterNext(opts: JsonAware & {
8
+ level: string;
9
+ }): void;
10
+ export declare function printNextStepsAfterCreate(opts: JsonAware & {
11
+ pushed: boolean;
12
+ tag?: string;
13
+ }): void;
14
+ export {};
@@ -0,0 +1,43 @@
1
+ import { color, info } from "./ui.js";
2
+ /** A dim "next step" line: an arrow, a label, and the literal command. */
3
+ function step(label, command) {
4
+ info(` ${color.dim("→")} ${label}: ${color.cyan(command)}`);
5
+ }
6
+ /** Shown when a command needs a config but none exists yet. */
7
+ export function printFirstRunHint(opts = {}) {
8
+ if (opts.json)
9
+ return;
10
+ info("");
11
+ info(color.bold("No tag spec yet."));
12
+ step("Define one", "tagsmith init");
13
+ }
14
+ export function printNextStepsAfterInit(opts = {}) {
15
+ if (opts.json)
16
+ return;
17
+ info("");
18
+ info(color.bold("Next steps:"));
19
+ step("Inspect existing tags", "tagsmith list");
20
+ step("Preview the next tag", "tagsmith next");
21
+ }
22
+ export function printNextStepsAfterNext(opts) {
23
+ if (opts.json)
24
+ return;
25
+ info("");
26
+ info(color.bold("Next step:"));
27
+ step("Create this tag", `tagsmith create -l ${opts.level}`);
28
+ }
29
+ export function printNextStepsAfterCreate(opts) {
30
+ if (opts.json)
31
+ return;
32
+ info("");
33
+ info(color.bold("Next step:"));
34
+ if (opts.pushed) {
35
+ step("Review your tags", "tagsmith list");
36
+ }
37
+ else if (opts.tag) {
38
+ step("Publish it", `git push origin ${opts.tag}`);
39
+ }
40
+ else {
41
+ step("Publish it", "git push --tags");
42
+ }
43
+ }
@@ -0,0 +1,17 @@
1
+ /** Injectable IO so the walkthrough is testable without a real TTY. */
2
+ export interface GuideIO {
3
+ intro(message: string): void;
4
+ outro(message: string): void;
5
+ note(message: string): void;
6
+ cancel(message: string): void;
7
+ confirm(message: string): Promise<boolean | symbol>;
8
+ isCancel(value: unknown): boolean;
9
+ }
10
+ /** Default GuideIO backed by @clack/prompts; exercised only at runtime (unit tests inject a fake IO). */
11
+ export declare const clackIO: GuideIO;
12
+ /**
13
+ * Interactive walkthrough of init -> list -> next -> create.
14
+ * Read-only: it can run `init` (with your confirmation) and preview
15
+ * `list`/`next`, but it never creates a real tag.
16
+ */
17
+ export declare function runGuide(cwd: string, io?: GuideIO): Promise<number>;
@@ -0,0 +1,61 @@
1
+ import * as p from "@clack/prompts";
2
+ import { configExists } from "../core/config.js";
3
+ import { runInit } from "./init.js";
4
+ import { runList } from "./list.js";
5
+ import { runNext } from "./next.js";
6
+ /** Default GuideIO backed by @clack/prompts; exercised only at runtime (unit tests inject a fake IO). */
7
+ export const clackIO = {
8
+ intro: (m) => p.intro(m),
9
+ outro: (m) => p.outro(m),
10
+ note: (m) => p.note(m),
11
+ cancel: (m) => p.cancel(m),
12
+ confirm: (message) => p.confirm({ message }),
13
+ isCancel: (v) => p.isCancel(v),
14
+ };
15
+ /**
16
+ * Interactive walkthrough of init -> list -> next -> create.
17
+ * Read-only: it can run `init` (with your confirmation) and preview
18
+ * `list`/`next`, but it never creates a real tag.
19
+ */
20
+ export async function runGuide(cwd, io = clackIO) {
21
+ io.intro("tagsmith guide");
22
+ io.note("Tagsmith defines a tag spec for this repo, then safely computes and " +
23
+ "creates the next git tag. Let's walk through it.");
24
+ // Step 1 - init
25
+ if (await configExists(cwd)) {
26
+ io.note("A .tagsmith.json already exists, so we'll skip init.");
27
+ }
28
+ else {
29
+ const answer = await io.confirm("No .tagsmith.json yet. Run `tagsmith init` now to create one?");
30
+ if (io.isCancel(answer)) {
31
+ io.cancel("Guide cancelled. Run `tagsmith init` whenever you're ready.");
32
+ return 0;
33
+ }
34
+ if (answer === true) {
35
+ // User already confirmed via io.confirm above, so run init
36
+ // non-interactively with defaults (no second round of prompts).
37
+ const code = await runInit(cwd, { yes: true, hints: false });
38
+ if (code !== 0) {
39
+ io.cancel("init did not complete. Re-run `tagsmith guide` to retry.");
40
+ return 0;
41
+ }
42
+ }
43
+ else {
44
+ io.note("No problem. Run `tagsmith init` later, then `tagsmith guide` again " +
45
+ "to see the rest.");
46
+ io.outro("That's the first step — `tagsmith init`.");
47
+ return 0;
48
+ }
49
+ }
50
+ // Step 2 - list (read-only preview)
51
+ io.note("`tagsmith list` shows existing tags, sorted and validated:");
52
+ await runList(cwd, {});
53
+ // Step 3 - next (read-only preview)
54
+ io.note("`tagsmith next` previews the next tag without creating it:");
55
+ await runNext(cwd, { hints: false });
56
+ // Step 4 - create (explained only; we never create here)
57
+ io.note("When you're ready, `tagsmith create -l patch` creates that tag. " +
58
+ "Add `--push` to publish it. (The guide won't create anything.)");
59
+ io.outro("You're set. Try `tagsmith next` then `tagsmith create`.");
60
+ return 0;
61
+ }
@@ -0,0 +1,5 @@
1
+ import type { ResolvedConfig } from "../core/config.js";
2
+ /** Extra JSON fields when config was inferred rather than loaded from disk. */
3
+ export declare function implicitConfigJson(resolved: ResolvedConfig): Record<string, string>;
4
+ /** One-time human notice that implicit semver defaults are in use. */
5
+ export declare function printImplicitConfigNotice(resolved: ResolvedConfig, json?: boolean): void;