@doki-land/nifty 0.0.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/src/gitmoji.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { CommitRecord, GithubAuthor, ParsedSubject, RangeInfo, ReleaseSection, TagInfo } from "./types.js";
2
+ import { loadNiftyNative, type GitmojiExports } from "./native.js";
3
+
4
+ /**
5
+ * Nifty default convention: every commit subject starts with a gitmoji followed by a space.
6
+ */
7
+ export class Gitmoji {
8
+ constructor(
9
+ private readonly wasm: GitmojiExports,
10
+ private readonly defaultAuthorMapJson = "{}",
11
+ ) {}
12
+
13
+ static open(authorMapJson = "{}"): Gitmoji {
14
+ const native = loadNiftyNative();
15
+ return new Gitmoji(native.gitmoji, authorMapJson);
16
+ }
17
+
18
+ knownGitmojis(): string[] {
19
+ return this.wasm["known-gitmojis"]();
20
+ }
21
+
22
+ validateSubject(subject: string): boolean {
23
+ return this.wasm["validate-subject"](subject);
24
+ }
25
+
26
+ parseSubject(subject: string): ParsedSubject {
27
+ return this.wasm["parse-subject"](subject);
28
+ }
29
+
30
+ formatSubject(gitmoji: string, body: string): string {
31
+ return this.wasm["format-subject"](gitmoji, body);
32
+ }
33
+
34
+ stripGitmoji(subject: string): string {
35
+ return this.wasm["strip-gitmoji"](subject);
36
+ }
37
+
38
+ leadingGitmoji(subject: string): string | undefined {
39
+ return this.wasm["leading-gitmoji"](subject);
40
+ }
41
+
42
+ sectionForGitmoji(gitmoji?: string): ReleaseSection {
43
+ return this.wasm["section-for-gitmoji"](gitmoji);
44
+ }
45
+
46
+ parseNoreplyEmail(email: string): GithubAuthor | undefined {
47
+ return this.wasm["parse-noreply-email"](email);
48
+ }
49
+
50
+ resolveGithubAuthor(email: string, authorMapJson = this.defaultAuthorMapJson): GithubAuthor | undefined {
51
+ return this.wasm["resolve-github-author"](email, authorMapJson);
52
+ }
53
+
54
+ displayLogin(author: GithubAuthor): string {
55
+ return this.wasm["display-login"](author);
56
+ }
57
+
58
+ profileUrl(author: GithubAuthor): string {
59
+ return this.wasm["profile-url"](author);
60
+ }
61
+
62
+ avatarUrl(author: GithubAuthor): string {
63
+ return this.wasm["avatar-url"](author);
64
+ }
65
+
66
+ authorMention(email: string, authorName: string, authorMapJson = this.defaultAuthorMapJson): string {
67
+ return this.wasm["author-mention"](email, authorName, authorMapJson);
68
+ }
69
+
70
+ commitBullet(body: string, email: string, authorName: string, authorMapJson = this.defaultAuthorMapJson): string {
71
+ return this.wasm["commit-bullet"](body, email, authorName, authorMapJson);
72
+ }
73
+ }
74
+
75
+ export function createGitmoji(): Gitmoji {
76
+ return Gitmoji.open();
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @doki-land/nifty — gitmoji-first commit conventions and gix-backed git helpers.
3
+ *
4
+ * Build native bindings first: `npm run build:napi` at the repo root.
5
+ * CLI (`nifty update`, `nifty lint`, `nifty upload`) is provided via the `nifty` bin.
6
+ */
7
+
8
+ export type {
9
+ CommitRecord,
10
+ GithubAuthor,
11
+ ParsedSubject,
12
+ RangeInfo,
13
+ ReleaseSection,
14
+ TagInfo,
15
+ } from "./types.js";
16
+ export { Git, createGit, type ResolveRangeOptions } from "./git.js";
17
+ export { Github, createGithub, type LookupUserOptions } from "./github.js";
18
+ export { Gitmoji, createGitmoji } from "./gitmoji.js";
19
+ export { Nifty, createNifty, type NiftyOpenOptions } from "./nifty.js";
20
+ export {
21
+ defineConfig,
22
+ loadConfig,
23
+ findConfigFile,
24
+ authorMapToJson,
25
+ detectProjectLayout,
26
+ discoverPackageDirs,
27
+ findCargoManifest,
28
+ findPackageManifest,
29
+ type LoadConfigOptions,
30
+ type LoadedNiftyConfig,
31
+ type NiftyAuthorEntry,
32
+ type NiftyConfig,
33
+ type NiftyConfigEnv,
34
+ type ProjectKind,
35
+ type ProjectLayout,
36
+ } from "./config/index.js";
package/src/native.ts ADDED
@@ -0,0 +1,277 @@
1
+ import { createRequire } from "node:module";
2
+ import type {
3
+ CommitRecord,
4
+ GithubAuthor,
5
+ ParsedSubject,
6
+ RangeInfo,
7
+ ReleaseSection,
8
+ TagInfo,
9
+ } from "./types.js";
10
+
11
+ export type GitmojiExports = {
12
+ "known-gitmojis": () => string[];
13
+ "validate-subject": (subject: string) => boolean;
14
+ "parse-subject": (subject: string) => ParsedSubject;
15
+ "format-subject": (gitmoji: string, body: string) => string;
16
+ "strip-gitmoji": (subject: string) => string;
17
+ "leading-gitmoji": (subject: string) => string | undefined;
18
+ "section-for-gitmoji": (gitmoji: string | undefined) => ReleaseSection;
19
+ "parse-noreply-email": (email: string) => GithubAuthor | undefined;
20
+ "resolve-github-author": (email: string, authorMapJson: string) => GithubAuthor | undefined;
21
+ "display-login": (author: GithubAuthor) => string;
22
+ "profile-url": (author: GithubAuthor) => string;
23
+ "avatar-url": (author: GithubAuthor) => string;
24
+ "author-mention": (email: string, authorName: string, authorMapJson: string) => string;
25
+ "commit-bullet": (body: string, email: string, authorName: string, authorMapJson: string) => string;
26
+ };
27
+
28
+ export type GitExports = {
29
+ "discover-root": (startPath: string) => string;
30
+ "list-version-tags": (repoRoot: string) => string[];
31
+ "list-tag-infos": (repoRoot: string) => Array<{ name: string; "short-hash": string }>;
32
+ "format-tag-list": (repoRoot: string) => string;
33
+ "resolve-range": (
34
+ repoRoot: string,
35
+ version: string | undefined,
36
+ fromRef: string | undefined,
37
+ toRef: string | undefined,
38
+ ) => { version: string; "from-ref"?: string; "to-ref": string };
39
+ "collect-commits": (
40
+ repoRoot: string,
41
+ fromRef: string | undefined,
42
+ toRef: string,
43
+ ) => Array<{
44
+ hash: string;
45
+ email: string;
46
+ author: string;
47
+ subject: string;
48
+ body: string;
49
+ gitmoji?: string;
50
+ section: string;
51
+ }>;
52
+ "detect-github-repo": (repoRoot: string) => string | undefined;
53
+ "parse-github-remote": (url: string) => string | undefined;
54
+ };
55
+
56
+ export type PublishReport = {
57
+ root: string;
58
+ order: string[];
59
+ published: string[];
60
+ skipped: string[];
61
+ };
62
+
63
+ export type TrustReport = {
64
+ root: string;
65
+ configured: string[];
66
+ skipped: string[];
67
+ failed: string[];
68
+ };
69
+
70
+ export type PublisherExports = {
71
+ "publish-workspace": (options: {
72
+ cwd?: string;
73
+ dryRun?: boolean;
74
+ tag?: string;
75
+ access?: string;
76
+ npm?: string;
77
+ otp?: string;
78
+ totpSecret?: string;
79
+ token?: string;
80
+ }) => PublishReport;
81
+ "trust-workspace": (options: {
82
+ cwd?: string;
83
+ dryRun?: boolean;
84
+ only?: string;
85
+ npm?: string;
86
+ otp?: string;
87
+ totpSecret?: string;
88
+ token?: string;
89
+ }) => TrustReport;
90
+ };
91
+
92
+ export type NiftyNative = {
93
+ gitmoji: GitmojiExports;
94
+ git: GitExports;
95
+ publisher: PublisherExports;
96
+ };
97
+
98
+ type NativeBinding = {
99
+ gitmojiKnownGitmojis: () => string[];
100
+ gitmojiValidateSubject: (subject: string) => boolean;
101
+ gitmojiParseSubject: (subject: string) => ParsedSubject;
102
+ gitmojiFormatSubject: (gitmoji: string, body: string) => string;
103
+ gitmojiStripGitmoji: (subject: string) => string;
104
+ gitmojiLeadingGitmoji: (subject: string) => string | null;
105
+ gitmojiSectionForGitmoji: (gitmoji?: string | null) => string;
106
+ gitmojiParseNoreplyEmail: (email: string) => GithubAuthor | null;
107
+ gitmojiResolveGithubAuthor: (email: string, authorMapJson: string) => GithubAuthor | null;
108
+ gitmojiDisplayLogin: (author: GithubAuthor) => string;
109
+ gitmojiProfileUrl: (author: GithubAuthor) => string;
110
+ gitmojiAvatarUrl: (author: GithubAuthor) => string;
111
+ gitmojiAuthorMention: (email: string, authorName: string, authorMapJson: string) => string;
112
+ gitmojiCommitBullet: (body: string, email: string, authorName: string, authorMapJson: string) => string;
113
+ gitDiscoverRoot: (startPath: string) => string;
114
+ gitListVersionTags: (repoRoot: string) => string[];
115
+ gitListTagInfos: (repoRoot: string) => Array<{ name: string; shortHash: string }>;
116
+ gitFormatTagList: (repoRoot: string) => string;
117
+ gitResolveRange: (
118
+ repoRoot: string,
119
+ version?: string | null,
120
+ fromRef?: string | null,
121
+ toRef?: string | null,
122
+ ) => { version: string; fromRef?: string; toRef: string };
123
+ gitCollectCommits: (
124
+ repoRoot: string,
125
+ fromRef: string | null | undefined,
126
+ toRef: string,
127
+ ) => CommitRecord[];
128
+ gitDetectGithubRepo: (repoRoot: string) => string | null;
129
+ gitParseGithubRemote: (url: string) => string | null;
130
+ publisherPublishWorkspace: (options: {
131
+ cwd?: string | null;
132
+ dryRun?: boolean | null;
133
+ tag?: string | null;
134
+ access?: string | null;
135
+ npm?: string | null;
136
+ otp?: string | null;
137
+ totpSecret?: string | null;
138
+ token?: string | null;
139
+ }) => PublishReport;
140
+ publisherTrustWorkspace: (options: {
141
+ cwd?: string | null;
142
+ dryRun?: boolean | null;
143
+ only?: string | null;
144
+ npm?: string | null;
145
+ otp?: string | null;
146
+ totpSecret?: string | null;
147
+ token?: string | null;
148
+ }) => TrustReport;
149
+ };
150
+
151
+ const PLATFORM_PACKAGES: Record<string, string> = {
152
+ "win32-x64": "@doki-land/nifty-win32-x64",
153
+ "linux-x64": "@doki-land/nifty-linux-x64",
154
+ "linux-arm64": "@doki-land/nifty-linux-arm64",
155
+ "darwin-x64": "@doki-land/nifty-darwin-x64",
156
+ "darwin-arm64": "@doki-land/nifty-darwin-arm64",
157
+ };
158
+
159
+ function mapAuthor(raw: GithubAuthor | null | undefined): GithubAuthor | undefined {
160
+ if (!raw) {
161
+ return undefined;
162
+ }
163
+ return {
164
+ id: raw.id !== undefined && raw.id !== null ? BigInt(raw.id) : undefined,
165
+ login: raw.login ?? undefined,
166
+ };
167
+ }
168
+
169
+ function wrapBinding(binding: NativeBinding): NiftyNative {
170
+ return {
171
+ gitmoji: {
172
+ "known-gitmojis": () => binding.gitmojiKnownGitmojis(),
173
+ "validate-subject": (subject) => binding.gitmojiValidateSubject(subject),
174
+ "parse-subject": (subject) => binding.gitmojiParseSubject(subject),
175
+ "format-subject": (gitmoji, body) => binding.gitmojiFormatSubject(gitmoji, body),
176
+ "strip-gitmoji": (subject) => binding.gitmojiStripGitmoji(subject),
177
+ "leading-gitmoji": (subject) => binding.gitmojiLeadingGitmoji(subject) ?? undefined,
178
+ "section-for-gitmoji": (gitmoji) => binding.gitmojiSectionForGitmoji(gitmoji) as ReleaseSection,
179
+ "parse-noreply-email": (email) => mapAuthor(binding.gitmojiParseNoreplyEmail(email)),
180
+ "resolve-github-author": (email, authorMapJson) =>
181
+ mapAuthor(binding.gitmojiResolveGithubAuthor(email, authorMapJson)),
182
+ "display-login": (author) => binding.gitmojiDisplayLogin(author),
183
+ "profile-url": (author) => binding.gitmojiProfileUrl(author),
184
+ "avatar-url": (author) => binding.gitmojiAvatarUrl(author),
185
+ "author-mention": (email, authorName, authorMapJson) =>
186
+ binding.gitmojiAuthorMention(email, authorName, authorMapJson),
187
+ "commit-bullet": (body, email, authorName, authorMapJson) =>
188
+ binding.gitmojiCommitBullet(body, email, authorName, authorMapJson),
189
+ },
190
+ git: {
191
+ "discover-root": (startPath) => binding.gitDiscoverRoot(startPath),
192
+ "list-version-tags": (repoRoot) => binding.gitListVersionTags(repoRoot),
193
+ "list-tag-infos": (repoRoot) =>
194
+ binding.gitListTagInfos(repoRoot).map((tag) => ({
195
+ name: tag.name,
196
+ "short-hash": tag.shortHash,
197
+ })),
198
+ "format-tag-list": (repoRoot) => binding.gitFormatTagList(repoRoot),
199
+ "resolve-range": (repoRoot, version, fromRef, toRef) => {
200
+ const range = binding.gitResolveRange(repoRoot, version, fromRef, toRef);
201
+ return {
202
+ version: range.version,
203
+ "from-ref": range.fromRef,
204
+ "to-ref": range.toRef,
205
+ };
206
+ },
207
+ "collect-commits": (repoRoot, fromRef, toRef) => binding.gitCollectCommits(repoRoot, fromRef, toRef),
208
+ "detect-github-repo": (repoRoot) => binding.gitDetectGithubRepo(repoRoot) ?? undefined,
209
+ "parse-github-remote": (url) => binding.gitParseGithubRemote(url) ?? undefined,
210
+ },
211
+ publisher: {
212
+ "publish-workspace": (options) =>
213
+ binding.publisherPublishWorkspace({
214
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
215
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
216
+ ...(options.tag !== undefined ? { tag: options.tag } : {}),
217
+ ...(options.access !== undefined ? { access: options.access } : {}),
218
+ ...(options.npm !== undefined ? { npm: options.npm } : {}),
219
+ ...(options.otp !== undefined ? { otp: options.otp } : {}),
220
+ ...(options.totpSecret !== undefined ? { totpSecret: options.totpSecret } : {}),
221
+ ...(options.token !== undefined ? { token: options.token } : {}),
222
+ }),
223
+ "trust-workspace": (options) =>
224
+ binding.publisherTrustWorkspace({
225
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
226
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
227
+ ...(options.only !== undefined ? { only: options.only } : {}),
228
+ ...(options.npm !== undefined ? { npm: options.npm } : {}),
229
+ ...(options.otp !== undefined ? { otp: options.otp } : {}),
230
+ ...(options.totpSecret !== undefined ? { totpSecret: options.totpSecret } : {}),
231
+ ...(options.token !== undefined ? { token: options.token } : {}),
232
+ }),
233
+ },
234
+ };
235
+ }
236
+
237
+ let cached: NiftyNative | undefined;
238
+
239
+ /** Load the platform-specific Node-API binary from `@doki-land/nifty-<platform>`. */
240
+ export function loadNiftyNative(): NiftyNative {
241
+ if (cached) {
242
+ return cached;
243
+ }
244
+ const key = `${process.platform}-${process.arch}`;
245
+ const pkg = PLATFORM_PACKAGES[key];
246
+ if (!pkg) {
247
+ throw new Error(`Unsupported platform for Nifty native bindings: ${key}`);
248
+ }
249
+ const require = createRequire(import.meta.url);
250
+ const binding = require(pkg).default as NativeBinding;
251
+ cached = wrapBinding(binding);
252
+ return cached;
253
+ }
254
+
255
+ export function mapTagInfo(raw: { name: string; "short-hash": string }): TagInfo {
256
+ return { name: raw.name, shortHash: raw["short-hash"] };
257
+ }
258
+
259
+ export function mapRangeInfo(raw: { version: string; "from-ref"?: string; "to-ref": string }): RangeInfo {
260
+ return {
261
+ version: raw.version,
262
+ fromRef: raw["from-ref"],
263
+ toRef: raw["to-ref"],
264
+ };
265
+ }
266
+
267
+ export function mapCommitRecord(raw: CommitRecord): CommitRecord {
268
+ return {
269
+ hash: raw.hash,
270
+ email: raw.email,
271
+ author: raw.author,
272
+ subject: raw.subject,
273
+ body: raw.body,
274
+ gitmoji: raw.gitmoji,
275
+ section: raw.section as ReleaseSection,
276
+ };
277
+ }
package/src/nifty.ts ADDED
@@ -0,0 +1,95 @@
1
+ import {
2
+ authorMapToJson,
3
+ detectProjectLayout,
4
+ loadConfig,
5
+ type LoadConfigOptions,
6
+ type NiftyConfig,
7
+ type ProjectLayout,
8
+ } from "./config/index.js";
9
+
10
+ import { Git } from "./git.js";
11
+ import { Github } from "./github.js";
12
+ import { Gitmoji } from "./gitmoji.js";
13
+ import { loadNiftyNative } from "./native.js";
14
+
15
+ export type NiftyOpenOptions = NiftyConfig &
16
+ LoadConfigOptions & {
17
+ /** When `false`, skip loading `nifty.config.ts/js`. Inline options still apply. */
18
+ config?: false;
19
+ };
20
+
21
+ function resolveRoots(config: NiftyConfig, layout: ProjectLayout): NiftyConfig {
22
+ return {
23
+ ...config,
24
+ repoRoot: config.repoRoot ?? layout.root,
25
+ cargoRoot:
26
+ config.cargoRoot ??
27
+ layout.cargoWorkspaceRoot ??
28
+ (layout.cargoManifest ? layout.cargoManifest.replace(/[/\\]Cargo\.toml$/, "") : undefined),
29
+ npmRoot:
30
+ config.npmRoot ??
31
+ layout.npmWorkspaceRoot ??
32
+ (layout.packageManifest ? layout.packageManifest.replace(/[/\\]package\.json$/, "") : undefined),
33
+ };
34
+ }
35
+
36
+ /** Combined Nifty engine: gitmoji conventions + gix repository reads + GitHub API. */
37
+ export class Nifty {
38
+ readonly gitmoji: Gitmoji;
39
+ readonly git: Git;
40
+ readonly github: Github;
41
+ readonly config: NiftyConfig;
42
+ readonly layout: ProjectLayout;
43
+ readonly configFile?: string;
44
+
45
+ private constructor(
46
+ gitmoji: Gitmoji,
47
+ git: Git,
48
+ github: Github,
49
+ config: NiftyConfig,
50
+ layout: ProjectLayout,
51
+ configFile?: string,
52
+ ) {
53
+ this.gitmoji = gitmoji;
54
+ this.git = git;
55
+ this.github = github;
56
+ this.config = config;
57
+ this.layout = layout;
58
+ this.configFile = configFile;
59
+ }
60
+
61
+ static async open(options: NiftyOpenOptions = {}): Promise<Nifty> {
62
+ const cwd = options.cwd ?? process.cwd();
63
+ const layout = detectProjectLayout(cwd);
64
+ const { config: skipConfig, configFile, env, githubToken, authorMap, repoRoot, cargoRoot, npmRoot } = options;
65
+ const loaded =
66
+ skipConfig === false
67
+ ? { config: {} as NiftyConfig }
68
+ : await loadConfig({ cwd, configFile, env: { layout, ...env } });
69
+ const merged = resolveRoots(
70
+ {
71
+ ...loaded.config,
72
+ ...(githubToken !== undefined ? { githubToken } : {}),
73
+ ...(authorMap !== undefined ? { authorMap } : {}),
74
+ ...(repoRoot !== undefined ? { repoRoot } : {}),
75
+ ...(cargoRoot !== undefined ? { cargoRoot } : {}),
76
+ ...(npmRoot !== undefined ? { npmRoot } : {}),
77
+ },
78
+ layout,
79
+ );
80
+
81
+ const native = loadNiftyNative();
82
+ return new Nifty(
83
+ new Gitmoji(native.gitmoji, authorMapToJson(merged.authorMap)),
84
+ new Git(native.git),
85
+ Github.open(merged.githubToken),
86
+ merged,
87
+ layout,
88
+ loaded.configFile,
89
+ );
90
+ }
91
+ }
92
+
93
+ export async function createNifty(options: NiftyOpenOptions = {}): Promise<Nifty> {
94
+ return Nifty.open(options);
95
+ }
package/src/types.ts ADDED
@@ -0,0 +1,40 @@
1
+ /** GitHub author resolved from noreply email or `author-github.json`. */
2
+ export type GithubAuthor = {
3
+ id?: bigint;
4
+ login?: string;
5
+ };
6
+
7
+ /** Nifty gitmoji-parsed commit subject. */
8
+ export type ParsedSubject = {
9
+ gitmoji?: string;
10
+ body: string;
11
+ /** `features` | `fixes` | `breaking` | `other` */
12
+ section: string;
13
+ };
14
+
15
+ /** Release note section keys (Nifty gitmoji grouping). */
16
+ export type ReleaseSection = "features" | "fixes" | "breaking" | "other";
17
+
18
+ /** Semver tag with short hash. */
19
+ export type TagInfo = {
20
+ name: string;
21
+ shortHash: string;
22
+ };
23
+
24
+ /** Resolved release tag range. */
25
+ export type RangeInfo = {
26
+ version: string;
27
+ fromRef?: string;
28
+ toRef: string;
29
+ };
30
+
31
+ /** Commit from repository history with Nifty gitmoji fields. */
32
+ export type CommitRecord = {
33
+ hash: string;
34
+ email: string;
35
+ author: string;
36
+ subject: string;
37
+ body: string;
38
+ gitmoji?: string;
39
+ section: ReleaseSection;
40
+ };