@gavin-hu/my-pi-agent 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -31
- package/README.zh-CN.md +40 -26
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +49 -39
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -7
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/view/todo-screen.ts +122 -0
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +27 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +1 -1
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Changelog state, for the `project-docs` skill.
|
|
3
|
-
*
|
|
4
|
-
* The skill's judgement is the model's; the counting is not. This prints what a
|
|
5
|
-
* changelog can be asked mechanically — the file, the last tag, the commits since it,
|
|
6
|
-
* the `[Unreleased]` categories, the empty and duplicate headings, the scope prefixes
|
|
7
|
-
* in use, and the link references — so an audit starts from facts instead of a
|
|
8
|
-
* hand-run `git log` whose flags and column are remembered differently each time.
|
|
9
|
-
*
|
|
10
|
-
* It ships inside the skill and resolves against the skill's own directory, so it
|
|
11
|
-
* imports nothing from the package's `lib/`: an installed copy has `skills/` and
|
|
12
|
-
* nothing else.
|
|
13
|
-
*
|
|
14
|
-
* Usage: `bun <skill>/scripts/state.ts [dir]`
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
-
import { join, relative, resolve } from "node:path";
|
|
19
|
-
|
|
20
|
-
/** One `###` category under `[Unreleased]`, as the file spells it. */
|
|
21
|
-
export interface Category {
|
|
22
|
-
/** The heading text, exactly as written: `Fixed`, not `Fix`. */
|
|
23
|
-
name: string;
|
|
24
|
-
/** 1-based line of the heading. */
|
|
25
|
-
line: number;
|
|
26
|
-
/** Top-level bullets between this heading and the next one. */
|
|
27
|
-
bullets: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** What the file's text alone says; `gitFacts` adds the part that needs a repository. */
|
|
31
|
-
export interface State {
|
|
32
|
-
/** Whether the first paragraphs name Keep a Changelog and Semantic Versioning. */
|
|
33
|
-
keepAChangelog: boolean;
|
|
34
|
-
/** The `[Unreleased]` section, when the file keeps one. */
|
|
35
|
-
unreleased?: { line: number; categories: Category[]; bullets: number };
|
|
36
|
-
/** Every dated `## [X.Y.Z]` heading, newest first, which is the order released sections appear in. */
|
|
37
|
-
dated: { version: string; line: number }[];
|
|
38
|
-
/** A heading text used twice inside one section, with every line it occurs on. */
|
|
39
|
-
duplicates: { name: string; lines: number[] }[];
|
|
40
|
-
/** Scope prefixes and how often each appears, most used first. */
|
|
41
|
-
scopes: { name: string; count: number }[];
|
|
42
|
-
/** The link references the file defines, lowercased, and the dated sections that lack one. */
|
|
43
|
-
refs: { unreleased: boolean; dated: number; missing: string[] };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** What the repository adds: the tag the window starts at, and what is in the window. */
|
|
47
|
-
export interface GitFacts {
|
|
48
|
-
/** The newest tag, when the repository has one. */
|
|
49
|
-
tag?: string;
|
|
50
|
-
/** Subject lines of the non-merge commits since the tag, newest first. */
|
|
51
|
-
commits: string[];
|
|
52
|
-
/** Why a fact is absent, in words the report can print: no repository, no tags. */
|
|
53
|
-
note?: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Keep a Changelog's `## [Unreleased]`, and the un-bracketed spelling some files use. */
|
|
57
|
-
const UNRELEASED = /^\[?unreleased\]?$/i;
|
|
58
|
-
/** A dated section heading: `[0.17.0] - 2025-06-01`, `0.17.0`, or a project's own suffix. */
|
|
59
|
-
const DATED = /^\[?(\d+\.\d+\.\d+[^\s\]]*)\]?/;
|
|
60
|
-
const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
|
|
61
|
-
const FENCE = /^\s*(?:```|~~~)/;
|
|
62
|
-
const BULLET = /^[-*+]\s+/;
|
|
63
|
-
/** A bullet's scope: backticked, bold, or a bare `area:` prefix. Not a path, and not prose. */
|
|
64
|
-
const SCOPE = /^[-*+]\s+(?:`([^`]+)`|\*\*([^*]+)\*\*|([A-Za-z][A-Za-z0-9./-]{0,24}))\s*:/;
|
|
65
|
-
const LINK_REF = /^\[([^\]]+)\]:\s*\S+/;
|
|
66
|
-
|
|
67
|
-
/** Report caps, so a long history stays one screen. */
|
|
68
|
-
export const MAX_COMMITS = 40;
|
|
69
|
-
export const MAX_SCOPES = 6;
|
|
70
|
-
|
|
71
|
-
interface Heading {
|
|
72
|
-
level: number;
|
|
73
|
-
text: string;
|
|
74
|
-
line: number;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** The changelog a project root holds, in the order projects name one. */
|
|
78
|
-
export const CHANGELOG_NAMES = [
|
|
79
|
-
"CHANGELOG.md",
|
|
80
|
-
"CHANGELOG",
|
|
81
|
-
"CHANGES.md",
|
|
82
|
-
"HISTORY.md",
|
|
83
|
-
"NEWS.md",
|
|
84
|
-
join("docs", "CHANGELOG.md"),
|
|
85
|
-
join("docs", "changelog.md"),
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
/** The first changelog file in `dir`, as an absolute path, or undefined when it has none. */
|
|
89
|
-
export function findChangelog(dir: string): string | undefined {
|
|
90
|
-
const root = resolve(dir);
|
|
91
|
-
for (const name of CHANGELOG_NAMES) {
|
|
92
|
-
const candidate = join(root, name);
|
|
93
|
-
if (existsSync(candidate)) return candidate;
|
|
94
|
-
}
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Every heading outside a fence, so a `## example` in a code block is not a section. */
|
|
99
|
-
function headingsOf(markdown: string): Heading[] {
|
|
100
|
-
const headings: Heading[] = [];
|
|
101
|
-
let fenced = false;
|
|
102
|
-
markdown.split("\n").forEach((text, index) => {
|
|
103
|
-
if (FENCE.test(text)) {
|
|
104
|
-
fenced = !fenced;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (fenced) return;
|
|
108
|
-
const match = HEADING.exec(text);
|
|
109
|
-
if (match !== null) headings.push({ level: match[1].length, text: match[2].trim(), line: index + 1 });
|
|
110
|
-
});
|
|
111
|
-
return headings;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** The scope a bullet leads with, if it leads with one. */
|
|
115
|
-
function scopeOf(line: string): string | undefined {
|
|
116
|
-
const match = SCOPE.exec(line);
|
|
117
|
-
if (match === null) return undefined;
|
|
118
|
-
return (match[1] ?? match[2] ?? match[3]).trim();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Bullet count and scopes over the given 1-based inclusive line window, outside fences. */
|
|
122
|
-
function scanBullets(lines: readonly string[], start: number, end: number): { bullets: number; scopes: string[] } {
|
|
123
|
-
let bullets = 0;
|
|
124
|
-
const scopes: string[] = [];
|
|
125
|
-
let fenced = false;
|
|
126
|
-
for (let line = start; line <= end && line <= lines.length; line++) {
|
|
127
|
-
const text = lines[line - 1];
|
|
128
|
-
if (FENCE.test(text)) {
|
|
129
|
-
fenced = !fenced;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (fenced || !BULLET.test(text)) continue;
|
|
133
|
-
bullets++;
|
|
134
|
-
const scope = scopeOf(text);
|
|
135
|
-
if (scope !== undefined) scopes.push(scope);
|
|
136
|
-
}
|
|
137
|
-
return { bullets, scopes };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Read the file's text into the facts the report prints. Pure, so a fixture can drive it. */
|
|
141
|
-
export function readState(markdown: string): State {
|
|
142
|
-
const lines = markdown.split("\n");
|
|
143
|
-
const headings = headingsOf(markdown);
|
|
144
|
-
const preamble = lines.slice(0, 20).join("\n");
|
|
145
|
-
|
|
146
|
-
const unreleasedAt = headings.findIndex((heading) => heading.level === 2 && UNRELEASED.test(heading.text));
|
|
147
|
-
const dated = headings
|
|
148
|
-
.filter((heading) => heading.level === 2)
|
|
149
|
-
.flatMap((heading) => {
|
|
150
|
-
const version = DATED.exec(heading.text)?.[1];
|
|
151
|
-
return version === undefined ? [] : [{ version, line: heading.line }];
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const scopes = new Map<string, number>();
|
|
155
|
-
for (const scope of scanBullets(lines, 1, lines.length).scopes) {
|
|
156
|
-
scopes.set(scope, (scopes.get(scope) ?? 0) + 1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const duplicates = duplicateHeadings(headings);
|
|
160
|
-
|
|
161
|
-
const refs = new Map<string, number>();
|
|
162
|
-
for (const line of lines) {
|
|
163
|
-
const name = LINK_REF.exec(line)?.[1];
|
|
164
|
-
if (name !== undefined) refs.set(name.toLowerCase(), (refs.get(name.toLowerCase()) ?? 0) + 1);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let unreleased: State["unreleased"];
|
|
168
|
-
if (unreleasedAt !== -1) {
|
|
169
|
-
const heading = headings[unreleasedAt];
|
|
170
|
-
const nextSection = headings.slice(unreleasedAt + 1).find((entry) => entry.level <= 2);
|
|
171
|
-
const windowEnd = nextSection === undefined ? lines.length : nextSection.line - 1;
|
|
172
|
-
const inner = headings.filter((entry) => entry.line > heading.line && entry.line <= windowEnd);
|
|
173
|
-
const categories: Category[] = [];
|
|
174
|
-
inner
|
|
175
|
-
.filter((entry) => entry.level === 3)
|
|
176
|
-
.forEach((entry, index) => {
|
|
177
|
-
const next = inner.filter((item) => item.level === 3)[index + 1];
|
|
178
|
-
const categoryEnd = next === undefined ? windowEnd : next.line - 1;
|
|
179
|
-
categories.push({
|
|
180
|
-
name: entry.text,
|
|
181
|
-
line: entry.line,
|
|
182
|
-
bullets: scanBullets(lines, entry.line + 1, categoryEnd).bullets,
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
unreleased = {
|
|
186
|
-
line: heading.line,
|
|
187
|
-
categories,
|
|
188
|
-
bullets: categories.reduce((total, category) => total + category.bullets, 0),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
keepAChangelog: /keep a changelog/i.test(preamble) && /semantic versioning/i.test(preamble),
|
|
194
|
-
unreleased,
|
|
195
|
-
dated,
|
|
196
|
-
duplicates,
|
|
197
|
-
scopes: [...scopes.entries()]
|
|
198
|
-
.map(([name, count]) => ({ name, count }))
|
|
199
|
-
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
|
|
200
|
-
refs: {
|
|
201
|
-
unreleased: refs.has("unreleased"),
|
|
202
|
-
dated: dated.length,
|
|
203
|
-
missing: dated.filter((entry) => !refs.has(entry.version.toLowerCase())).map((entry) => entry.version),
|
|
204
|
-
},
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** A trim with an ellipsis, so one long line cannot wrap the report. */
|
|
209
|
-
function clip(text: string, width: number): string {
|
|
210
|
-
return text.length > width ? `${text.slice(0, width - 1)}…` : text;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* A heading repeated inside one section, which is what a reader sees as one category
|
|
215
|
-
* split in two. The same `### Fixed` under each release is the format working, not a
|
|
216
|
-
* duplicate, so headings are grouped by the `##` section that contains them.
|
|
217
|
-
*/
|
|
218
|
-
function duplicateHeadings(headings: readonly Heading[]): { name: string; lines: number[] }[] {
|
|
219
|
-
const groups = new Map<string, { name: string; lines: number[] }>();
|
|
220
|
-
let section = "top";
|
|
221
|
-
for (const heading of headings) {
|
|
222
|
-
if (heading.level <= 2) section = heading.level === 2 ? heading.text.toLowerCase() : "top";
|
|
223
|
-
const key = `${section}\u0000${heading.text.toLowerCase()}`;
|
|
224
|
-
const group = groups.get(key) ?? { name: heading.text, lines: [] };
|
|
225
|
-
group.lines.push(heading.line);
|
|
226
|
-
groups.set(key, group);
|
|
227
|
-
}
|
|
228
|
-
return [...groups.values()].filter((group) => group.lines.length > 1);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* The report. Every line is a fact with its source, in the order an audit uses them:
|
|
233
|
-
* the file, the window, the unreleased section, then what is wrong with the file, then
|
|
234
|
-
* what the window contains.
|
|
235
|
-
*/
|
|
236
|
-
export function formatState(file: string, state: State, git: GitFacts): string {
|
|
237
|
-
const out: string[] = [];
|
|
238
|
-
const preamble = state.keepAChangelog ? "Keep a Changelog preamble" : "no Keep a Changelog preamble";
|
|
239
|
-
out.push(`changelog ${file} · ${preamble}`);
|
|
240
|
-
|
|
241
|
-
const count = `${git.commits.length} commit${git.commits.length === 1 ? "" : "s"} since, merges skipped`;
|
|
242
|
-
out.push(`last tag ${git.tag === undefined ? (git.note ?? "no tags") : `${git.tag} · ${count}`}`);
|
|
243
|
-
|
|
244
|
-
out.push(
|
|
245
|
-
state.unreleased === undefined
|
|
246
|
-
? "unreleased missing: the file has no `[Unreleased]` section"
|
|
247
|
-
: `unreleased line ${state.unreleased.line} · ${state.unreleased.bullets} bullets`,
|
|
248
|
-
);
|
|
249
|
-
if (state.unreleased !== undefined) {
|
|
250
|
-
const named = state.unreleased.categories.map((category) => `${category.name} ${category.bullets}`);
|
|
251
|
-
out.push(`categories ${named.length === 0 ? "none" : named.join(" · ")} (file order)`);
|
|
252
|
-
const empty = state.unreleased.categories.filter((category) => category.bullets === 0).map((item) => item.name);
|
|
253
|
-
out.push(`empty ${empty.length === 0 ? "none" : empty.join(" · ")}`);
|
|
254
|
-
}
|
|
255
|
-
out.push(
|
|
256
|
-
state.duplicates.length === 0
|
|
257
|
-
? "duplicate none"
|
|
258
|
-
: `duplicate ${state.duplicates
|
|
259
|
-
.slice(0, 4)
|
|
260
|
-
.map((entry) => `${entry.name} at lines ${entry.lines.join(", ")}`)
|
|
261
|
-
.join(" · ")}`,
|
|
262
|
-
);
|
|
263
|
-
const scopes = state.scopes.slice(0, MAX_SCOPES).map((entry) => `${entry.name} ${entry.count}`);
|
|
264
|
-
const rest = state.scopes.length - MAX_SCOPES;
|
|
265
|
-
out.push(
|
|
266
|
-
`scopes ${scopes.length === 0 ? "none" : `${scopes.join(" · ")}${rest > 0 ? ` · … ${rest} more` : ""}`}`,
|
|
267
|
-
);
|
|
268
|
-
out.push(
|
|
269
|
-
`refs [Unreleased] ${state.refs.unreleased ? "yes" : "no"} · dated refs ${state.refs.dated - state.refs.missing.length}/${state.refs.dated}${
|
|
270
|
-
state.refs.missing.length === 0 ? "" : `, missing ${state.refs.missing.slice(0, 5).join(", ")}`
|
|
271
|
-
}`,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
if (git.commits.length > 0) {
|
|
275
|
-
out.push(`commits since ${git.tag ?? "the beginning"}`);
|
|
276
|
-
for (const commit of git.commits.slice(0, MAX_COMMITS)) out.push(` ${clip(commit, 110)}`);
|
|
277
|
-
const more = git.commits.length - MAX_COMMITS;
|
|
278
|
-
if (more > 0) out.push(` … ${more} more`);
|
|
279
|
-
}
|
|
280
|
-
return `${out.join("\n")}\n`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Run one git command, returning its trimmed stdout or a reason it produced none. */
|
|
284
|
-
function git(args: readonly string[], cwd: string): { text?: string; error?: string } {
|
|
285
|
-
const result = Bun.spawnSync({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" });
|
|
286
|
-
if (result.exitCode !== 0) return { error: result.stderr.toString().trim() || `git ${args[0]} failed` };
|
|
287
|
-
return { text: result.stdout.toString().trim() };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* The tag to audit from, and the non-merge commits after it. A repository with no
|
|
292
|
-
* tags has no window, and a directory outside git has neither; both are reported as a
|
|
293
|
-
* note rather than an error, because the file half of the report is still useful.
|
|
294
|
-
*/
|
|
295
|
-
export function gitFacts(cwd: string): GitFacts {
|
|
296
|
-
const inside = git(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
297
|
-
if (inside.text !== "true") return { commits: [], note: "not a git repository" };
|
|
298
|
-
|
|
299
|
-
const described = git(["describe", "--tags", "--abbrev=0"], cwd);
|
|
300
|
-
const tag = described.text === "" ? undefined : described.text;
|
|
301
|
-
const range = tag === undefined ? ["HEAD"] : [`${tag}..HEAD`];
|
|
302
|
-
const log = git(["log", "--no-merges", "--format=%h %s", ...range], cwd);
|
|
303
|
-
const commits = (log.text ?? "").split("\n").filter((line) => line.trim() !== "");
|
|
304
|
-
return { tag, commits, note: tag === undefined ? "no tags" : undefined };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (import.meta.main) {
|
|
308
|
-
const dir = resolve(process.argv[2] ?? ".");
|
|
309
|
-
const changelog = findChangelog(dir);
|
|
310
|
-
if (changelog === undefined) {
|
|
311
|
-
console.error(`no changelog in ${dir}: looked for ${CHANGELOG_NAMES.join(", ")}`);
|
|
312
|
-
process.exit(1);
|
|
313
|
-
}
|
|
314
|
-
const state = readState(readFileSync(changelog, "utf-8"));
|
|
315
|
-
process.stdout.write(formatState(relative(dir, changelog), state, gitFacts(dir)));
|
|
316
|
-
}
|