@doki-land/nifty 0.0.5 → 0.0.7

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.
@@ -9,9 +9,14 @@
9
9
  "cli.cmd.bump": "Align workspace crate and package versions",
10
10
  "cli.cmd.publish": "Publish workspace npm packages (OIDC or token + TOTP)",
11
11
  "cli.cmd.trust": "Configure npm Trusted Publisher for workspace packages",
12
- "cli.cmd.reword": "Rewrite commit messages at the git object layer",
13
- "cli.cmd.reword.export": "Export a JSON reword map template",
14
- "cli.cmd.reword.rewrite": "Rewrite commits from a JSON map and update the branch ref",
12
+ "cli.cmd.commit": "Commit message export, apply, scan, and audit",
13
+ "cli.cmd.commit.export": "Export a JSON commit message map template",
14
+ "cli.cmd.commit.apply": "Apply commit message edits from a JSON map and update the branch ref",
15
+ "cli.cmd.commit.scan": "Scan commit messages for gitmoji and hygiene issues",
16
+ "cli.cmd.commit.audit": "Audit commit messages and fail on errors",
17
+ "cli.opt.with-lint": "Run commit lint after export and annotate the JSON map with violations",
18
+ "cli.opt.json": "Print commit lint output as JSON",
19
+ "cli.opt.errors-only": "Only report error-level commit lint diagnostics",
15
20
  "cli.cmd.retime": "Spread commit timestamps across a date range on a new branch",
16
21
  "cli.cmd.retime.root": "Retime all commits from repository root through tip",
17
22
  "cli.cmd.change-logs": "Draft release reference changelogs from git tags",
@@ -51,7 +56,7 @@
51
56
  "cli.opt.npm-token": "npm auth token (overrides NPM_TOKEN)",
52
57
  "cli.opt.base": "Exclusive range start commit (e.g. 34e1e665^)",
53
58
  "cli.opt.ref": "Branch ref to rewrite (default: HEAD)",
54
- "cli.opt.path": "Path to reword JSON map (default: reword.pending.json)",
59
+ "cli.opt.path": "Path to commit JSON map (default: commit.pending.json)",
55
60
  "cli.opt.start-date": "Random window start (YYYY-MM-DD or ISO datetime)",
56
61
  "cli.opt.end-date": "Random window end",
57
62
  "cli.opt.branch": "Output branch name (default: time-travel)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doki-land/nifty",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Gitmoji-first commit conventions and release helpers (Node-API + TypeScript)",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -24,9 +24,9 @@
24
24
  "jiti": "^2.4.2"
25
25
  },
26
26
  "optionalDependencies": {
27
- "@doki-land/nifty-win32-x64": "0.0.5",
28
- "@doki-land/nifty-linux-x64": "0.0.5",
29
- "@doki-land/nifty-darwin-arm64": "0.0.5"
27
+ "@doki-land/nifty-win32-x64": "0.0.7",
28
+ "@doki-land/nifty-linux-x64": "0.0.7",
29
+ "@doki-land/nifty-darwin-arm64": "0.0.7"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"
@@ -2,6 +2,8 @@ import { join } from "node:path";
2
2
 
3
3
  import type { Cli, ParsedOptions } from "@vmz/commander";
4
4
 
5
+ import { resolveAuthorMapPath } from "../config/authorMap.js";
6
+ import { detectProjectLayout } from "../config/detectProject.js";
5
7
  import { loadConfig } from "../config/loadConfig.js";
6
8
  import { loadNiftyNative } from "../native.js";
7
9
  import { bootstrapFromOptions } from "./context.js";
@@ -34,6 +36,8 @@ export async function cmdChangeLogsRender(options: ParsedOptions): Promise<numbe
34
36
  await bootstrapFromOptions(options);
35
37
  const cwd = cwdFrom(options) ?? process.cwd();
36
38
  const { config } = await loadConfig({ cwd, createIfMissing: false });
39
+ const layout = detectProjectLayout(cwd);
40
+ const repoRoot = config.repoRoot ?? layout.root;
37
41
  const native = loadNiftyNative();
38
42
  const report = native.history["changelog-render"]({
39
43
  cwd,
@@ -43,7 +47,7 @@ export async function cmdChangeLogsRender(options: ParsedOptions): Promise<numbe
43
47
  write: flag(options, "write"),
44
48
  tags: flag(options, "tags"),
45
49
  repo: str(options, "repo") ?? config.changelog?.repo,
46
- authorMap: str(options, "author-map") ?? config.changelog?.authorMap,
50
+ authorMap: str(options, "author-map") ?? resolveAuthorMapPath(config, repoRoot),
47
51
  releasesDir: str(options, "releases-dir") ?? config.changelog?.releasesDir,
48
52
  });
49
53
 
@@ -66,6 +70,8 @@ export async function cmdChangeLogsLookup(options: ParsedOptions): Promise<numbe
66
70
  await bootstrapFromOptions(options);
67
71
  const cwd = cwdFrom(options) ?? process.cwd();
68
72
  const { config } = await loadConfig({ cwd, createIfMissing: false });
73
+ const layout = detectProjectLayout(cwd);
74
+ const repoRoot = config.repoRoot ?? layout.root;
69
75
  const email = str(options, "email");
70
76
  const login = str(options, "login");
71
77
  if (!email && !login) {
@@ -76,7 +82,7 @@ export async function cmdChangeLogsLookup(options: ParsedOptions): Promise<numbe
76
82
  cwd,
77
83
  email,
78
84
  login,
79
- map: str(options, "map") ?? config.changelog?.authorMap,
85
+ map: str(options, "map") ?? resolveAuthorMapPath(config, repoRoot),
80
86
  githubToken: str(options, "github-token") ?? config.githubToken ?? process.env.GITHUB_TOKEN,
81
87
  fetch: flag(options, "fetch"),
82
88
  });
package/src/cli/cli.ts CHANGED
@@ -10,7 +10,7 @@ import { registerInstallNativeCommand } from "./install-native-cmd.js";
10
10
  import { registerLintCommands } from "./lint-cmd.js";
11
11
  import { registerPublishCommand } from "./publish-cmd.js";
12
12
  import { registerRetimeCommand } from "./retime-cmd.js";
13
- import { registerRewordCommand } from "./reword-cmd.js";
13
+ import { registerCommitCommand } from "./commit-cmd.js";
14
14
  import { registerTrustCommand } from "./trust-cmd.js";
15
15
  import { registerUpdateCommand } from "./update-cmd.js";
16
16
  import { registerUploadCommand } from "./upload-cmd.js";
@@ -30,7 +30,7 @@ function buildNiftyCli() {
30
30
  registerBumpCommand(cli);
31
31
  registerPublishCommand(cli);
32
32
  registerTrustCommand(cli);
33
- registerRewordCommand(cli);
33
+ registerCommitCommand(cli);
34
34
  registerRetimeCommand(cli);
35
35
  registerChangeLogsCommand(cli);
36
36
 
@@ -0,0 +1,226 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+
3
+ import type { Cli, ParsedOptions } from "@vmz/commander";
4
+
5
+ import { loadNiftyNative } from "../native.js";
6
+ import { bootstrapFromOptions } from "./context.js";
7
+ import { lintCommits, type LintOptions } from "./lint.js";
8
+ import { cwdFrom, flag, str, strList } from "./options.js";
9
+
10
+ export function registerCommitCommand(cli: Cli): void {
11
+ const commit = cli.command("commit", "cli.cmd.commit");
12
+
13
+ commit
14
+ .command("export", "cli.cmd.commit.export")
15
+ .option("--base <ref>", "cli.opt.base")
16
+ .option("--ref <ref>", "cli.opt.ref")
17
+ .option("--path <file>", "cli.opt.path")
18
+ .option("--with-lint", "cli.opt.with-lint")
19
+ .option("--errors-only", "cli.opt.errors-only")
20
+ .action(async (options) => cmdCommitExport(options));
21
+
22
+ commit
23
+ .command("apply", "cli.cmd.commit.apply")
24
+ .option("--base <ref>", "cli.opt.base")
25
+ .option("--ref <ref>", "cli.opt.ref")
26
+ .option("--path <file>", "cli.opt.path")
27
+ .option("--dry-run", "cli.opt.dry-run")
28
+ .action(async (options) => cmdCommitApply(options));
29
+
30
+ commit
31
+ .command("scan", "cli.cmd.commit.scan")
32
+ .option("--from <ref>", "cli.opt.from")
33
+ .option("--to <ref>", "cli.opt.to")
34
+ .option("-s, --subject <text>...", "cli.opt.subject")
35
+ .option("--json", "cli.opt.json")
36
+ .option("--errors-only", "cli.opt.errors-only")
37
+ .action(async (options) => cmdCommitScan(options));
38
+
39
+ commit
40
+ .command("audit", "cli.cmd.commit.audit")
41
+ .option("--from <ref>", "cli.opt.from")
42
+ .option("--to <ref>", "cli.opt.to")
43
+ .option("-s, --subject <text>...", "cli.opt.subject")
44
+ .option("--json", "cli.opt.json")
45
+ .option("--errors-only", "cli.opt.errors-only")
46
+ .action(async (options) => cmdCommitAudit(options));
47
+ }
48
+
49
+ export async function cmdCommitExport(options: ParsedOptions): Promise<number> {
50
+ await bootstrapFromOptions(options);
51
+ const base = requireStr(options, "base");
52
+ const path = str(options, "path") ?? "commit.pending.json";
53
+ const native = loadNiftyNative();
54
+ const report = native.history["commit-export"]({
55
+ cwd: cwdFrom(options),
56
+ base,
57
+ ref: str(options, "ref"),
58
+ path,
59
+ });
60
+ console.log(`export: wrote ${report.count} commit entry(ies) to ${report.path}`);
61
+
62
+ if (flag(options, "with-lint")) {
63
+ const errorCount = await lintAndAnnotateExport({
64
+ path,
65
+ from: base,
66
+ to: str(options, "ref") ?? "HEAD",
67
+ cwd: cwdFrom(options),
68
+ json: flag(options, "json"),
69
+ errorsOnly: flag(options, "errors-only"),
70
+ reportTitle: "commit export lint",
71
+ });
72
+ if (errorCount > 0) {
73
+ return 1;
74
+ }
75
+ }
76
+
77
+ return 0;
78
+ }
79
+
80
+ export async function cmdCommitApply(options: ParsedOptions): Promise<number> {
81
+ await bootstrapFromOptions(options);
82
+ const base = requireStr(options, "base");
83
+ const path = requireStr(options, "path");
84
+ const dryRun = flag(options, "dry-run");
85
+ const native = loadNiftyNative();
86
+ const report = native.history["commit-apply"]({
87
+ cwd: cwdFrom(options),
88
+ base,
89
+ ref: str(options, "ref"),
90
+ path,
91
+ dryRun,
92
+ });
93
+
94
+ if (dryRun) {
95
+ if (report.changes.length === 0) {
96
+ console.log("dry-run: no commit objects need rewriting");
97
+ return 0;
98
+ }
99
+ console.log(`dry-run: ${report.changes.length} commit object(s) would be rewritten on ${report.refName}\n`);
100
+ for (const change of report.changes) {
101
+ console.log(`${change.oldOid} ${change.oldSubject}`);
102
+ console.log(` -> ${change.newSubject}`);
103
+ if (change.parentsRelinked && !change.messageChanged) {
104
+ console.log(" (parent chain relink only)");
105
+ }
106
+ console.log();
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ console.log(
112
+ `applied ${report.changes.length} commit object(s); ${report.oldTip} -> ${report.newTip ?? report.oldTip}`,
113
+ );
114
+ for (const change of report.changes) {
115
+ console.log(` ${change.oldOid} ${change.newSubject}`);
116
+ }
117
+ return 0;
118
+ }
119
+
120
+ export async function cmdCommitScan(options: ParsedOptions): Promise<number> {
121
+ await bootstrapFromOptions(options);
122
+ await lintCommits({ ...commitLintOptions(options), reportTitle: "commit scan" }, false, true);
123
+ return 0;
124
+ }
125
+
126
+ export async function cmdCommitAudit(options: ParsedOptions): Promise<number> {
127
+ await bootstrapFromOptions(options);
128
+ const errorCount = await lintCommits(commitLintOptions(options), false, true);
129
+ return errorCount > 0 ? 1 : 0;
130
+ }
131
+
132
+ function commitLintOptions(options: ParsedOptions): LintOptions {
133
+ return {
134
+ subjects: strList(options, "subject"),
135
+ from: str(options, "from"),
136
+ to: str(options, "to"),
137
+ cwd: cwdFrom(options),
138
+ scanCargo: false,
139
+ commitOnly: true,
140
+ json: flag(options, "json"),
141
+ errorsOnly: flag(options, "errors-only"),
142
+ reportTitle: "commit audit",
143
+ };
144
+ }
145
+
146
+ type ExportDocument = {
147
+ version: number;
148
+ entries: Array<{
149
+ hash: string;
150
+ message: string;
151
+ violations?: Array<{
152
+ rule: string;
153
+ severity: "error" | "warning" | "info";
154
+ message: string;
155
+ }>;
156
+ }>;
157
+ };
158
+
159
+ async function lintAndAnnotateExport(options: {
160
+ path: string;
161
+ from: string;
162
+ to: string;
163
+ cwd: string;
164
+ json: boolean;
165
+ errorsOnly: boolean;
166
+ reportTitle: string;
167
+ }): Promise<number> {
168
+ const native = loadNiftyNative();
169
+ const report = native.lint.run({
170
+ cwd: options.cwd,
171
+ fromRef: options.from,
172
+ toRef: options.to,
173
+ commitOnly: true,
174
+ scanCargo: false,
175
+ });
176
+ const diagnostics = options.errorsOnly
177
+ ? report.diagnostics.filter((item) => item.severity === "error")
178
+ : report.diagnostics;
179
+
180
+ if (options.json) {
181
+ const { printCommitLintReport } = await import("./commit-lint-report.js");
182
+ printCommitLintReport(diagnostics, report.errorCount, report.warningCount, {
183
+ json: true,
184
+ errorsOnly: options.errorsOnly,
185
+ title: options.reportTitle,
186
+ });
187
+ } else {
188
+ const { printCommitLintReport } = await import("./commit-lint-report.js");
189
+ printCommitLintReport(diagnostics, report.errorCount, report.warningCount, {
190
+ errorsOnly: options.errorsOnly,
191
+ title: options.reportTitle,
192
+ });
193
+ }
194
+
195
+ const byHash = new Map<string, Array<{ rule: string; severity: "error" | "warning" | "info"; message: string }>>();
196
+ for (const item of diagnostics) {
197
+ if (!item.hash) {
198
+ continue;
199
+ }
200
+ const bucket = byHash.get(item.hash) ?? [];
201
+ bucket.push({ rule: item.rule, severity: item.severity, message: item.message });
202
+ byHash.set(item.hash, bucket);
203
+ }
204
+
205
+ const text = await readFile(options.path, "utf8");
206
+ const document = JSON.parse(text) as ExportDocument;
207
+ document.entries = document.entries.map((entry) => {
208
+ const violations = byHash.get(entry.hash);
209
+ if (!violations || violations.length === 0) {
210
+ return entry;
211
+ }
212
+ return { ...entry, violations };
213
+ });
214
+ await writeFile(options.path, `${JSON.stringify(document, null, 2)}\n`, "utf8");
215
+ const violationCount = document.entries.filter((entry) => entry.violations && entry.violations.length > 0).length;
216
+ console.log(`export: annotated ${violationCount} commit entry(ies) with lint violations in ${options.path}`);
217
+ return report.errorCount;
218
+ }
219
+
220
+ function requireStr(options: ParsedOptions, key: string): string {
221
+ const value = str(options, key);
222
+ if (!value) {
223
+ throw new Error(`missing required --${key}`);
224
+ }
225
+ return value;
226
+ }
@@ -0,0 +1,110 @@
1
+ export type CommitLintDiagnostic = {
2
+ rule: string;
3
+ severity: "error" | "warning" | "info";
4
+ message: string;
5
+ subject?: string;
6
+ hash?: string;
7
+ path?: string;
8
+ line?: number;
9
+ };
10
+
11
+ export type CommitLintReportOptions = {
12
+ json?: boolean;
13
+ errorsOnly?: boolean;
14
+ title?: string;
15
+ };
16
+
17
+ type CommitLintGroup = {
18
+ hash?: string;
19
+ subject?: string;
20
+ items: CommitLintDiagnostic[];
21
+ };
22
+
23
+ export function printCommitLintReport(
24
+ diagnostics: CommitLintDiagnostic[],
25
+ errorCount: number,
26
+ warningCount: number,
27
+ options: CommitLintReportOptions = {},
28
+ ): void {
29
+ const filtered = options.errorsOnly
30
+ ? diagnostics.filter((item) => item.severity === "error")
31
+ : diagnostics;
32
+
33
+ if (options.json) {
34
+ const payload = {
35
+ errorCount,
36
+ warningCount,
37
+ diagnostics: filtered,
38
+ groups: groupCommitDiagnostics(filtered),
39
+ summaryByRule: summarizeByRule(filtered),
40
+ };
41
+ console.log(JSON.stringify(payload, null, 2));
42
+ return;
43
+ }
44
+
45
+ const title = options.title ?? "commit lint";
46
+ if (filtered.length === 0) {
47
+ console.log(`${title}: no issues found`);
48
+ return;
49
+ }
50
+
51
+ const groups = groupCommitDiagnostics(filtered);
52
+ const commitCount = groups.filter((group) => group.hash || group.subject).length;
53
+ console.log(`${title}: ${errorCount} error(s), ${warningCount} warning(s) in ${commitCount} commit(s)\n`);
54
+
55
+ for (const group of groups) {
56
+ printCommitGroup(group);
57
+ }
58
+
59
+ const summary = summarizeByRule(filtered.filter((item) => item.severity === "error"));
60
+ if (summary.length > 0) {
61
+ console.log("summary by rule:");
62
+ for (const [rule, count] of summary) {
63
+ console.log(` ${rule} ${count}`);
64
+ }
65
+ console.log();
66
+ }
67
+ }
68
+
69
+ function groupCommitDiagnostics(diagnostics: CommitLintDiagnostic[]): CommitLintGroup[] {
70
+ const groups = new Map<string, CommitLintGroup>();
71
+ const order: string[] = [];
72
+
73
+ for (const item of diagnostics) {
74
+ const key = item.hash ?? item.subject ?? "__inline__";
75
+ if (!groups.has(key)) {
76
+ groups.set(key, {
77
+ hash: item.hash,
78
+ subject: item.subject,
79
+ items: [],
80
+ });
81
+ order.push(key);
82
+ }
83
+ groups.get(key)?.items.push(item);
84
+ }
85
+
86
+ return order.map((key) => groups.get(key)!);
87
+ }
88
+
89
+ function summarizeByRule(diagnostics: CommitLintDiagnostic[]): Array<[string, number]> {
90
+ const counts = new Map<string, number>();
91
+ for (const item of diagnostics) {
92
+ counts.set(item.rule, (counts.get(item.rule) ?? 0) + 1);
93
+ }
94
+ return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
95
+ }
96
+
97
+ function printCommitGroup(group: CommitLintGroup): void {
98
+ if (group.hash || group.subject) {
99
+ const hash = group.hash ? `${group.hash.slice(0, 8)} ` : "";
100
+ const subject = group.subject ?? "(no subject)";
101
+ console.log(`${hash}${subject}`);
102
+ } else {
103
+ console.log("(inline subject lint)");
104
+ }
105
+
106
+ for (const item of group.items) {
107
+ console.log(` ${item.severity} ${item.rule} ${item.message}`);
108
+ }
109
+ console.log();
110
+ }
package/src/cli/format.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { loadConfig, resolveFormatConfig } from "../config/index.js";
1
2
  import { loadNiftyNative } from "../native.js";
2
3
 
3
4
  export type FormatOptions = {
@@ -6,10 +7,18 @@ export type FormatOptions = {
6
7
  };
7
8
 
8
9
  export async function formatWorkspace(options: FormatOptions): Promise<number> {
10
+ const cwd = options.cwd ?? process.cwd();
11
+ const { config } = await loadConfig({ cwd, createIfMissing: false, env: { command: "format" } });
12
+ const resolved = resolveFormatConfig(config.format);
9
13
  const native = loadNiftyNative();
10
14
  const report = native.formatter.run({
11
- cwd: options.cwd,
15
+ cwd,
12
16
  check: options.check,
17
+ includes: resolved.includes,
18
+ excludes: resolved.excludes,
19
+ rust: resolved.rust,
20
+ javascript: resolved.javascript,
21
+ style: resolved.style,
13
22
  });
14
23
 
15
24
  if (report.errors.length > 0) {
@@ -19,9 +28,11 @@ export async function formatWorkspace(options: FormatOptions): Promise<number> {
19
28
  }
20
29
 
21
30
  if (report.formatted === 0 && report.unchanged > 0 && report.errors.length === 0) {
22
- console.log("format: no changes needed");
31
+ console.log(`format: no changes needed (preset ${resolved.preset})`);
23
32
  } else if (report.errors.length === 0) {
24
- console.log(`format: ${report.formatted} file(s) updated, ${report.unchanged} unchanged`);
33
+ console.log(
34
+ `format: ${report.formatted} file(s) updated, ${report.unchanged} unchanged (preset ${resolved.preset})`,
35
+ );
25
36
  }
26
37
 
27
38
  return report.errors.length > 0 ? 1 : 0;
package/src/cli/lint.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { loadConfig } from "../config/loadConfig.js";
2
2
  import { loadNiftyNative } from "../native.js";
3
+ import { printCommitLintReport, type CommitLintDiagnostic } from "./commit-lint-report.js";
3
4
 
4
5
  export type LintOptions = {
5
6
  subjects: string[];
@@ -7,17 +8,13 @@ export type LintOptions = {
7
8
  to?: string;
8
9
  cwd?: string;
9
10
  scanCargo?: boolean;
11
+ commitOnly?: boolean;
12
+ json?: boolean;
13
+ errorsOnly?: boolean;
14
+ reportTitle?: string;
10
15
  };
11
16
 
12
- type LintDiagnostic = {
13
- rule: string;
14
- severity: "error" | "warning" | "info";
15
- message: string;
16
- subject?: string;
17
- hash?: string;
18
- path?: string;
19
- line?: number;
20
- };
17
+ type LintDiagnostic = CommitLintDiagnostic;
21
18
 
22
19
  type LintRuleConfig = {
23
20
  id: string;
@@ -33,7 +30,11 @@ export async function runLint(argv: string[], check: boolean): Promise<void> {
33
30
  }
34
31
  }
35
32
 
36
- export async function lintCommits(options: LintOptions, check: boolean): Promise<number> {
33
+ export async function lintCommits(
34
+ options: LintOptions,
35
+ check: boolean,
36
+ commitOnly = options.commitOnly ?? false,
37
+ ): Promise<number> {
37
38
  const native = loadNiftyNative();
38
39
  const cwd = options.cwd ?? process.cwd();
39
40
  const { config } = await loadConfig({ cwd, createIfMissing: false });
@@ -51,25 +52,35 @@ export async function lintCommits(options: LintOptions, check: boolean): Promise
51
52
  subjects: options.subjects.length > 0 ? options.subjects : undefined,
52
53
  rules,
53
54
  scanCargo: options.scanCargo,
55
+ commitOnly: commitOnly || options.commitOnly,
54
56
  });
55
57
 
56
- for (const item of report.diagnostics) {
57
- printDiagnostic(item);
58
- }
59
-
60
- if (report.diagnostics.length > 0) {
61
- console.log(`lint: ${report.errorCount} error(s), ${report.warningCount} warning(s)`);
58
+ if (commitOnly || options.commitOnly) {
59
+ printCommitLintReport(report.diagnostics, report.errorCount, report.warningCount, {
60
+ json: options.json,
61
+ errorsOnly: options.errorsOnly,
62
+ title: options.reportTitle ?? "commit lint",
63
+ });
62
64
  } else {
63
- console.log("lint: no issues found");
65
+ for (const item of report.diagnostics) {
66
+ printWorkspaceDiagnostic(item);
67
+ }
68
+
69
+ if (report.diagnostics.length > 0) {
70
+ console.log(`lint: ${report.errorCount} error(s), ${report.warningCount} warning(s)`);
71
+ } else {
72
+ console.log("lint: no issues found");
73
+ }
64
74
  }
65
75
 
66
76
  return report.errorCount;
67
77
  }
68
78
 
69
- function printDiagnostic(item: LintDiagnostic): void {
79
+ function printWorkspaceDiagnostic(item: LintDiagnostic): void {
70
80
  const prefix = item.severity === "error" ? "error" : item.severity === "warning" ? "warning" : "info";
71
81
  if (item.subject) {
72
- console.log(`${prefix} [${item.rule}] ${item.message} (${item.subject})`);
82
+ const hash = item.hash ? `${item.hash.slice(0, 8)} ` : "";
83
+ console.log(`${prefix} [${item.rule}] ${hash}${item.message} (${item.subject})`);
73
84
  return;
74
85
  }
75
86
  const location =
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+
4
+ import type { NiftyConfig } from "./types.js";
5
+
6
+ /** Default path relative to repo root (email → GitHub author JSON). */
7
+ export const DEFAULT_AUTHOR_MAP_PATH = "documentation/maintenance/author-github.json";
8
+
9
+ /** Resolve configured author map path to an absolute filesystem path. */
10
+ export function resolveAuthorMapPath(config: NiftyConfig, repoRoot: string): string {
11
+ const configured = config.authorMap ?? DEFAULT_AUTHOR_MAP_PATH;
12
+ return isAbsolute(configured) ? configured : join(repoRoot, configured);
13
+ }
14
+
15
+ /** Load author map file contents for native helpers expecting JSON text. */
16
+ export function loadAuthorMapJson(repoRoot: string, authorMapPath?: string): string {
17
+ const abs = authorMapPath
18
+ ? isAbsolute(authorMapPath)
19
+ ? authorMapPath
20
+ : join(repoRoot, authorMapPath)
21
+ : resolveAuthorMapPath({}, repoRoot);
22
+ if (!existsSync(abs)) {
23
+ return "{}";
24
+ }
25
+ return readFileSync(abs, "utf8");
26
+ }
@@ -0,0 +1,64 @@
1
+ import type { NiftyFormatConfig, NiftyFormatPreset, NiftyFormatStyleConfig } from "./types.js";
2
+
3
+ export type ResolvedFormatOptions = {
4
+ preset: NiftyFormatPreset;
5
+ includes?: string[];
6
+ excludes?: string[];
7
+ rust?: boolean;
8
+ javascript?: boolean;
9
+ style: NiftyFormatStyleConfig;
10
+ };
11
+
12
+ const DEFAULT_FORMAT_STYLE: NiftyFormatStyleConfig = {
13
+ indentStyle: "space",
14
+ indentWidth: 4,
15
+ lineWidth: 144,
16
+ quoteStyle: "single",
17
+ };
18
+
19
+ const NIFTY_PRESET_INCLUDES = [
20
+ "scripts/**",
21
+ "projects/packages/**",
22
+ "projects/conformance/**",
23
+ "projects/dashboard/**",
24
+ "package.json",
25
+ "nifty.config.ts",
26
+ ] as const;
27
+
28
+ /** VMZ hybrid monorepo — JS/TS surfaces only, never whole-repo walk. */
29
+ const VMZ_INCLUDES = [
30
+ "scripts/**",
31
+ "packages/runtimes/**",
32
+ "packages/examples/**",
33
+ "packages/editors/**",
34
+ "packages/ui/**",
35
+ "packages/plugins/**",
36
+ "packages/content/**",
37
+ "packages/homepage/**",
38
+ "package.json",
39
+ "nifty.config.ts",
40
+ ] as const;
41
+
42
+ const PRESET_INCLUDES: Record<NiftyFormatPreset, readonly string[] | undefined> = {
43
+ default: undefined,
44
+ nifty: NIFTY_PRESET_INCLUDES,
45
+ "npm-tools": NIFTY_PRESET_INCLUDES,
46
+ vmz: VMZ_INCLUDES,
47
+ };
48
+
49
+ /** Merge `nifty.config` `format` with built-in preset defaults. */
50
+ export function resolveFormatConfig(format: NiftyFormatConfig | undefined): ResolvedFormatOptions {
51
+ const preset = format?.preset ?? "default";
52
+ const presetIncludes = PRESET_INCLUDES[preset];
53
+ return {
54
+ preset,
55
+ includes: format?.includes ?? (presetIncludes ? [...presetIncludes] : undefined),
56
+ excludes: format?.excludes,
57
+ rust: format?.rust,
58
+ javascript: format?.javascript,
59
+ style: {
60
+ ...DEFAULT_FORMAT_STYLE,
61
+ ...format?.style,
62
+ },
63
+ };
64
+ }
@@ -8,7 +8,11 @@ export {
8
8
  type ProjectLayout,
9
9
  } from "./detectProject.js";
10
10
  export {
11
- authorMapToJson,
11
+ DEFAULT_AUTHOR_MAP_PATH,
12
+ loadAuthorMapJson,
13
+ resolveAuthorMapPath,
14
+ } from "./authorMap.js";
15
+ export {
12
16
  defaultConfigPath,
13
17
  ensureConfigFile,
14
18
  findConfigFile,
@@ -16,10 +20,14 @@ export {
16
20
  type LoadConfigOptions,
17
21
  type LoadedNiftyConfig,
18
22
  } from "./loadConfig.js";
23
+ export { resolveFormatConfig, type ResolvedFormatOptions } from "./formatPresets.js";
19
24
  export {
20
25
  CONFIG_FILE_NAMES,
21
26
  type NiftyAuthorEntry,
22
27
  type NiftyConfig,
23
28
  type NiftyConfigEnv,
24
29
  type NiftyConfigExport,
30
+ type NiftyFormatConfig,
31
+ type NiftyFormatPreset,
32
+ type NiftyFormatStyleConfig,
25
33
  } from "./types.js";
@@ -28,7 +28,7 @@ export type LoadedNiftyConfig = {
28
28
  };
29
29
 
30
30
  const DEFAULT_CONFIG_TEMPLATE = `// Nifty project configuration.
31
- // See @doki-land/nifty for defineConfig, githubToken, authorMap, and lint rules.
31
+ // See @doki-land/nifty for defineConfig, format presets, authorMap path, and lint rules.
32
32
  export default {};
33
33
  `;
34
34
 
@@ -120,10 +120,3 @@ export async function loadConfig(options: LoadConfigOptions = {}): Promise<Loade
120
120
  return { config, configFile, created };
121
121
  }
122
122
 
123
- /** Serialize `authorMap` for native helpers expecting JSON text. */
124
- export function authorMapToJson(authorMap: NiftyConfig["authorMap"]): string {
125
- if (!authorMap || Object.keys(authorMap).length === 0) {
126
- return "{}";
127
- }
128
- return JSON.stringify(authorMap);
129
- }
@@ -1,15 +1,46 @@
1
- /** GitHub author entry in `authorMap` (same shape as `author-github.json`). */
1
+ /** One email → GitHub author row in `author-github.json`. */
2
2
  export type NiftyAuthorEntry = {
3
3
  id?: number;
4
4
  login?: string;
5
5
  };
6
6
 
7
+ /** Built-in workspace format presets (`nifty.config` `format.preset`). */
8
+ export type NiftyFormatPreset = "default" | "nifty" | "npm-tools" | "vmz";
9
+
10
+ /** `oxc_formatter` style knobs (`nifty.config` `format.style`). */
11
+ export type NiftyFormatStyleConfig = {
12
+ /** Indent with spaces or tabs (default: `space`). */
13
+ indentStyle?: "space" | "tab";
14
+ /** Spaces or tabs per indent level (default: `4`). */
15
+ indentWidth?: number;
16
+ /** Soft line width (default: `144`). */
17
+ lineWidth?: number;
18
+ /** String quote preference (default: `single`). */
19
+ quoteStyle?: "single" | "double";
20
+ };
21
+
22
+ /** Workspace format options (`nifty format` reads `nifty.config` `format`). */
23
+ export type NiftyFormatConfig = {
24
+ /** Select built-in include roots for Nifty hybrid monorepos. */
25
+ preset?: NiftyFormatPreset;
26
+ /** Extra include globs (override preset list when set alone). */
27
+ includes?: string[];
28
+ /** Skip paths under these globs after includes expand. */
29
+ excludes?: string[];
30
+ /** Run `cargo fmt` (default: true when a Cargo workspace is detected). */
31
+ rust?: boolean;
32
+ /** Run `oxc_formatter` on JS/TS targets (default: true). */
33
+ javascript?: boolean;
34
+ /** Inline oxc formatter style (not an external config file path). */
35
+ style?: NiftyFormatStyleConfig;
36
+ };
37
+
7
38
  /** Nifty project configuration (`nifty.config.ts` / `nifty.config.js`). */
8
39
  export type NiftyConfig = {
9
40
  /** GitHub personal access token for email search / profile fetch. */
10
41
  githubToken?: string;
11
- /** Email → GitHub author mapping. */
12
- authorMap?: Record<string, NiftyAuthorEntry>;
42
+ /** Path to `author-github.json` (default: `documentation/maintenance/author-github.json`). */
43
+ authorMap?: string;
13
44
  /** Repository root override when discovering git metadata. */
14
45
  repoRoot?: string;
15
46
  /** Cargo workspace / crate root override. */
@@ -25,8 +56,6 @@ export type NiftyConfig = {
25
56
  changelog?: {
26
57
  /** GitHub `owner/repo` for contrib.rocks (default: parse `origin`). */
27
58
  repo?: string;
28
- /** Email → GitHub map path (default: `documentation/maintenance/author-github.json`). */
29
- authorMap?: string;
30
59
  /** `--write` output directory (default: `documentation/maintenance/releases`). */
31
60
  releasesDir?: string;
32
61
  };
@@ -38,6 +67,8 @@ export type NiftyConfig = {
38
67
  severity?: "error" | "warning" | "info";
39
68
  }>;
40
69
  };
70
+ /** Workspace format targets and engines (`nifty format`). */
71
+ format?: NiftyFormatConfig;
41
72
  };
42
73
 
43
74
  export type NiftyConfigExport = NiftyConfig | ((env: NiftyConfigEnv) => NiftyConfig | Promise<NiftyConfig>);
package/src/index.ts CHANGED
@@ -21,16 +21,23 @@ export {
21
21
  defineConfig,
22
22
  loadConfig,
23
23
  findConfigFile,
24
- authorMapToJson,
24
+ DEFAULT_AUTHOR_MAP_PATH,
25
+ loadAuthorMapJson,
26
+ resolveAuthorMapPath,
25
27
  detectProjectLayout,
26
28
  discoverPackageDirs,
27
29
  findCargoManifest,
28
30
  findPackageManifest,
29
31
  type LoadConfigOptions,
30
32
  type LoadedNiftyConfig,
33
+ resolveFormatConfig,
31
34
  type NiftyAuthorEntry,
32
35
  type NiftyConfig,
33
36
  type NiftyConfigEnv,
37
+ type NiftyFormatConfig,
38
+ type NiftyFormatPreset,
39
+ type NiftyFormatStyleConfig,
34
40
  type ProjectKind,
35
41
  type ProjectLayout,
42
+ type ResolvedFormatOptions,
36
43
  } from "./config/index.js";
package/src/native.ts CHANGED
@@ -85,12 +85,27 @@ export type RewordRewriteReport = {
85
85
  };
86
86
 
87
87
  export type HistoryExports = {
88
+ "commit-export": (options: {
89
+ cwd?: string;
90
+ base: string;
91
+ ref?: string;
92
+ path: string;
93
+ }) => { count: number; path: string };
94
+ "commit-apply": (options: {
95
+ cwd?: string;
96
+ base: string;
97
+ ref?: string;
98
+ path: string;
99
+ dryRun?: boolean;
100
+ }) => RewordRewriteReport;
101
+ /** @deprecated use `commit-export` */
88
102
  "reword-export": (options: {
89
103
  cwd?: string;
90
104
  base: string;
91
105
  ref?: string;
92
106
  path: string;
93
107
  }) => { count: number; path: string };
108
+ /** @deprecated use `commit-apply` */
94
109
  "reword-rewrite": (options: {
95
110
  cwd?: string;
96
111
  base: string;
@@ -160,6 +175,7 @@ export type LintRunOptions = {
160
175
  subjects?: string[];
161
176
  rules?: Array<{ id: string; enabled?: boolean; severity?: "error" | "warning" | "info" }>;
162
177
  scanCargo?: boolean;
178
+ commitOnly?: boolean;
163
179
  };
164
180
 
165
181
  export type LintReportRecord = {
@@ -183,8 +199,25 @@ export type FormatReportRecord = {
183
199
  errors: string[];
184
200
  };
185
201
 
202
+ export type FormatterStyleOptions = {
203
+ indentStyle?: string;
204
+ indentWidth?: number;
205
+ lineWidth?: number;
206
+ quoteStyle?: string;
207
+ };
208
+
209
+ export type FormatterRunOptions = {
210
+ cwd?: string;
211
+ check?: boolean;
212
+ includes?: string[];
213
+ excludes?: string[];
214
+ rust?: boolean;
215
+ javascript?: boolean;
216
+ style?: FormatterStyleOptions;
217
+ };
218
+
186
219
  export type FormatterExports = {
187
- run: (options: { cwd?: string; check?: boolean }) => FormatReportRecord;
220
+ run: (options: FormatterRunOptions) => FormatReportRecord;
188
221
  };
189
222
 
190
223
  export type PublisherExports = {
@@ -280,6 +313,31 @@ type NativeBinding = {
280
313
  totpSecret?: string | null;
281
314
  token?: string | null;
282
315
  }) => TrustReport;
316
+ gitToolsCommitExport: (options: {
317
+ cwd?: string | null;
318
+ base: string;
319
+ ref?: string | null;
320
+ path: string;
321
+ }) => { count: number; path: string };
322
+ gitToolsCommitApply: (options: {
323
+ cwd?: string | null;
324
+ base: string;
325
+ ref?: string | null;
326
+ path: string;
327
+ dryRun?: boolean | null;
328
+ }) => {
329
+ changes: Array<{
330
+ oldOid: string;
331
+ oldSubject: string;
332
+ newSubject: string;
333
+ parentsRelinked: boolean;
334
+ messageChanged: boolean;
335
+ }>;
336
+ oldTip: string;
337
+ newTip?: string | null;
338
+ refName: string;
339
+ dryRun: boolean;
340
+ };
283
341
  gitToolsRewordExport: (options: {
284
342
  cwd?: string | null;
285
343
  base: string;
@@ -355,6 +413,7 @@ type NativeBinding = {
355
413
  subjects?: string[] | null;
356
414
  rules?: Array<{ id: string; enabled?: boolean | null; severity?: string | null }> | null;
357
415
  scanCargo?: boolean | null;
416
+ commitOnly?: boolean | null;
358
417
  check?: boolean | null;
359
418
  }) => {
360
419
  diagnostics: Array<{
@@ -376,6 +435,7 @@ type NativeBinding = {
376
435
  subjects?: string[] | null;
377
436
  rules?: Array<{ id: string; enabled?: boolean | null; severity?: string | null }> | null;
378
437
  scanCargo?: boolean | null;
438
+ commitOnly?: boolean | null;
379
439
  check?: boolean | null;
380
440
  }) => {
381
441
  diagnostics: Array<{
@@ -391,7 +451,15 @@ type NativeBinding = {
391
451
  warningCount: number;
392
452
  };
393
453
  updaterRun: (options: { cwd?: string | null; interactive?: boolean | null }) => void;
394
- formatterRun: (options: { cwd?: string | null; check?: boolean | null }) => {
454
+ formatterRun: (options: {
455
+ cwd?: string | null;
456
+ check?: boolean | null;
457
+ includes?: string[] | null;
458
+ excludes?: string[] | null;
459
+ rust?: boolean | null;
460
+ javascript?: boolean | null;
461
+ style?: FormatterStyleOptions | null;
462
+ }) => {
395
463
  formatted: number;
396
464
  unchanged: number;
397
465
  errors: string[];
@@ -424,6 +492,7 @@ function mapLintOptions(options: LintRunOptions) {
424
492
  ...(options.subjects !== undefined ? { subjects: options.subjects } : {}),
425
493
  ...(options.rules !== undefined ? { rules: options.rules } : {}),
426
494
  ...(options.scanCargo !== undefined ? { scanCargo: options.scanCargo } : {}),
495
+ ...(options.commitOnly !== undefined ? { commitOnly: options.commitOnly } : {}),
427
496
  };
428
497
  }
429
498
 
@@ -498,6 +567,29 @@ function wrapBinding(binding: NativeBinding): NiftyNative {
498
567
  "parse-github-remote": (url) => binding.gitParseGithubRemote(url) ?? undefined,
499
568
  },
500
569
  history: {
570
+ "commit-export": (options) =>
571
+ binding.gitToolsCommitExport({
572
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
573
+ base: options.base,
574
+ ...(options.ref !== undefined ? { ref: options.ref } : {}),
575
+ path: options.path,
576
+ }),
577
+ "commit-apply": (options) => {
578
+ const report = binding.gitToolsCommitApply({
579
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
580
+ base: options.base,
581
+ ...(options.ref !== undefined ? { ref: options.ref } : {}),
582
+ path: options.path,
583
+ ...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
584
+ });
585
+ return {
586
+ changes: report.changes,
587
+ oldTip: report.oldTip,
588
+ newTip: report.newTip ?? undefined,
589
+ refName: report.refName,
590
+ dryRun: report.dryRun,
591
+ };
592
+ },
501
593
  "reword-export": (options) =>
502
594
  binding.gitToolsRewordExport({
503
595
  ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
@@ -586,10 +678,16 @@ function wrapBinding(binding: NativeBinding): NiftyNative {
586
678
  },
587
679
  },
588
680
  formatter: {
589
- run: (options) => binding.formatterRun({
590
- ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
591
- ...(options.check !== undefined ? { check: options.check } : {}),
592
- }),
681
+ run: (options) =>
682
+ binding.formatterRun({
683
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
684
+ ...(options.check !== undefined ? { check: options.check } : {}),
685
+ ...(options.includes !== undefined ? { includes: options.includes } : {}),
686
+ ...(options.excludes !== undefined ? { excludes: options.excludes } : {}),
687
+ ...(options.rust !== undefined ? { rust: options.rust } : {}),
688
+ ...(options.javascript !== undefined ? { javascript: options.javascript } : {}),
689
+ ...(options.style !== undefined ? { style: options.style } : {}),
690
+ }),
593
691
  },
594
692
  publisher: {
595
693
  "publish-workspace": (options) =>
package/src/nifty.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
- authorMapToJson,
3
2
  detectProjectLayout,
3
+ loadAuthorMapJson,
4
4
  loadConfig,
5
5
  type LoadConfigOptions,
6
6
  type NiftyConfig,
@@ -79,8 +79,9 @@ export class Nifty {
79
79
  );
80
80
 
81
81
  const native = loadNiftyNative();
82
+ const resolvedRepoRoot = merged.repoRoot ?? layout.root;
82
83
  return new Nifty(
83
- new Gitmoji(native.gitmoji, authorMapToJson(merged.authorMap)),
84
+ new Gitmoji(native.gitmoji, loadAuthorMapJson(resolvedRepoRoot, merged.authorMap)),
84
85
  new Git(native.git),
85
86
  Github.open(merged.githubToken),
86
87
  merged,
@@ -1,87 +0,0 @@
1
- import type { Cli, ParsedOptions } from "@vmz/commander";
2
-
3
- import { loadNiftyNative } from "../native.js";
4
- import { bootstrapFromOptions } from "./context.js";
5
- import { cwdFrom, flag, str } from "./options.js";
6
-
7
- export function registerRewordCommand(cli: Cli): void {
8
- const reword = cli.command("reword", "cli.cmd.reword");
9
-
10
- reword
11
- .command("export", "cli.cmd.reword.export")
12
- .option("--base <ref>", "cli.opt.base")
13
- .option("--ref <ref>", "cli.opt.ref")
14
- .option("--path <file>", "cli.opt.path")
15
- .action(async (options) => cmdRewordExport(options));
16
-
17
- reword
18
- .command("rewrite", "cli.cmd.reword.rewrite")
19
- .option("--base <ref>", "cli.opt.base")
20
- .option("--ref <ref>", "cli.opt.ref")
21
- .option("--path <file>", "cli.opt.path")
22
- .option("--dry-run", "cli.opt.dry-run")
23
- .action(async (options) => cmdRewordRewrite(options));
24
- }
25
-
26
- export async function cmdRewordExport(options: ParsedOptions): Promise<number> {
27
- await bootstrapFromOptions(options);
28
- const base = requireStr(options, "base");
29
- const path = str(options, "path") ?? "reword.pending.json";
30
- const native = loadNiftyNative();
31
- const report = native.history["reword-export"]({
32
- cwd: cwdFrom(options),
33
- base,
34
- ref: str(options, "ref"),
35
- path,
36
- });
37
- console.log(`export: wrote ${report.count} commit entry(ies) to ${report.path}`);
38
- return 0;
39
- }
40
-
41
- export async function cmdRewordRewrite(options: ParsedOptions): Promise<number> {
42
- await bootstrapFromOptions(options);
43
- const base = requireStr(options, "base");
44
- const path = requireStr(options, "path");
45
- const dryRun = flag(options, "dry-run");
46
- const native = loadNiftyNative();
47
- const report = native.history["reword-rewrite"]({
48
- cwd: cwdFrom(options),
49
- base,
50
- ref: str(options, "ref"),
51
- path,
52
- dryRun,
53
- });
54
-
55
- if (dryRun) {
56
- if (report.changes.length === 0) {
57
- console.log("dry-run: no commit objects need rewriting");
58
- return 0;
59
- }
60
- console.log(`dry-run: ${report.changes.length} commit object(s) would be rewritten on ${report.refName}\n`);
61
- for (const change of report.changes) {
62
- console.log(`${change.oldOid} ${change.oldSubject}`);
63
- console.log(` -> ${change.newSubject}`);
64
- if (change.parentsRelinked && !change.messageChanged) {
65
- console.log(" (parent chain relink only)");
66
- }
67
- console.log();
68
- }
69
- return 0;
70
- }
71
-
72
- console.log(
73
- `rewrote ${report.changes.length} commit object(s); ${report.oldTip} -> ${report.newTip ?? report.oldTip}`,
74
- );
75
- for (const change of report.changes) {
76
- console.log(` ${change.oldOid} ${change.newSubject}`);
77
- }
78
- return 0;
79
- }
80
-
81
- function requireStr(options: ParsedOptions, key: string): string {
82
- const value = str(options, key);
83
- if (!value) {
84
- throw new Error(`missing required --${key}`);
85
- }
86
- return value;
87
- }