@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/Readme.md +30 -0
- package/cli/nifty.mjs +11 -0
- package/package.json +35 -0
- package/src/cli/authArgs.ts +52 -0
- package/src/cli/bump.ts +237 -0
- package/src/cli/context.ts +26 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/lint.ts +109 -0
- package/src/cli/main.ts +64 -0
- package/src/cli/publish.ts +85 -0
- package/src/cli/resolveNpm.ts +16 -0
- package/src/cli/trust.ts +77 -0
- package/src/cli/update.ts +182 -0
- package/src/cli/upload.ts +280 -0
- package/src/cli/workspace.ts +124 -0
- package/src/config/defineConfig.ts +12 -0
- package/src/config/detectProject.ts +189 -0
- package/src/config/index.ts +25 -0
- package/src/config/loadConfig.ts +129 -0
- package/src/config/types.ts +43 -0
- package/src/git.ts +61 -0
- package/src/github.ts +184 -0
- package/src/gitmoji.ts +77 -0
- package/src/index.ts +36 -0
- package/src/native.ts +277 -0
- package/src/nifty.ts +95 -0
- package/src/types.ts +40 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @doki-land/nifty
|
|
2
|
+
|
|
3
|
+
**Gitmoji is the default Nifty convention.**
|
|
4
|
+
|
|
5
|
+
| Layer | Role |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `@doki-land/nifty-<platform>` | Node-API `.node`(gitmoji + gix) |
|
|
8
|
+
| `nifty-publisher` | Rust:`petgraph` 拓扑排序 + `npm publish` |
|
|
9
|
+
| `src/cli/` | CLI 实现(`update` / `lint` / `check` / `upload` / `bump` / `publish`) |
|
|
10
|
+
| `cli/nifty.mjs` | npm `bin` shim(jiti 加载 `src/cli`) |
|
|
11
|
+
| `src/` | TypeScript API |
|
|
12
|
+
|
|
13
|
+
Rust 侧无 `bin` / `cli`。
|
|
14
|
+
|
|
15
|
+
**本仓开发**(pnpm workspace,`projects/packages/*`):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm install # 链接 @doki-land/nifty → node_modules/.bin/nifty
|
|
19
|
+
pnpm run build:napi # 构建 native addon
|
|
20
|
+
pnpm exec nifty bump
|
|
21
|
+
pnpm exec nifty publish --dry-run
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**发布后消费**:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @doki-land/nifty
|
|
28
|
+
nifty bump
|
|
29
|
+
nifty publish --dry-run
|
|
30
|
+
```
|
package/cli/nifty.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
9
|
+
const { runCli } = await jiti.import(join(here, "../src/cli/main.ts"));
|
|
10
|
+
|
|
11
|
+
process.exit(await runCli(process.argv.slice(2)));
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"nifty": "cli/nifty.mjs"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"jiti": "^2.4.2"
|
|
7
|
+
},
|
|
8
|
+
"description": "Gitmoji-first commit conventions and release helpers (Node-API + TypeScript)",
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/node": "^22.13.10",
|
|
11
|
+
"typescript": "^5.8.3"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.ts",
|
|
15
|
+
"./cli": "./src/cli/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"cli/nifty.mjs",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"license": "MPL-2.0",
|
|
22
|
+
"name": "@doki-land/nifty",
|
|
23
|
+
"optionalDependencies": {
|
|
24
|
+
"@doki-land/nifty-darwin-arm64": "0.0.0",
|
|
25
|
+
"@doki-land/nifty-darwin-x64": "0.0.0",
|
|
26
|
+
"@doki-land/nifty-linux-arm64": "0.0.0",
|
|
27
|
+
"@doki-land/nifty-linux-x64": "0.0.0",
|
|
28
|
+
"@doki-land/nifty-win32-x64": "0.0.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"version": "0.0.0"
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { resolveNpmExecutable } from "./resolveNpm.js";
|
|
2
|
+
|
|
3
|
+
export type AuthCliOptions = {
|
|
4
|
+
otp?: string;
|
|
5
|
+
totpSecret?: string;
|
|
6
|
+
token?: string;
|
|
7
|
+
npm: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function parseAuthArgs(argv: string[]): AuthCliOptions {
|
|
11
|
+
let otp: string | undefined;
|
|
12
|
+
let totpSecret: string | undefined;
|
|
13
|
+
let token: string | undefined;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === "--otp") {
|
|
18
|
+
otp = argv[++i];
|
|
19
|
+
} else if (arg === "--totp-secret") {
|
|
20
|
+
totpSecret = argv[++i];
|
|
21
|
+
} else if (arg === "--token") {
|
|
22
|
+
token = argv[++i];
|
|
23
|
+
} else if (arg.startsWith("--otp=")) {
|
|
24
|
+
otp = arg.slice("--otp=".length);
|
|
25
|
+
} else if (arg.startsWith("--totp-secret=")) {
|
|
26
|
+
totpSecret = arg.slice("--totp-secret=".length);
|
|
27
|
+
} else if (arg.startsWith("--token=")) {
|
|
28
|
+
token = arg.slice("--token=".length);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
otp,
|
|
34
|
+
totpSecret,
|
|
35
|
+
token,
|
|
36
|
+
npm: resolveNpmExecutable(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function authPayload(options: AuthCliOptions): {
|
|
41
|
+
otp?: string;
|
|
42
|
+
totpSecret?: string;
|
|
43
|
+
token?: string;
|
|
44
|
+
npm: string;
|
|
45
|
+
} {
|
|
46
|
+
return {
|
|
47
|
+
...(options.otp !== undefined ? { otp: options.otp } : {}),
|
|
48
|
+
...(options.totpSecret !== undefined ? { totpSecret: options.totpSecret } : {}),
|
|
49
|
+
...(options.token !== undefined ? { token: options.token } : {}),
|
|
50
|
+
npm: options.npm,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/src/cli/bump.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { findWorkspaceRoot } from "./workspace.js";
|
|
5
|
+
|
|
6
|
+
export type BumpKind = "patch" | "minor" | "major";
|
|
7
|
+
|
|
8
|
+
export type BumpOptions = {
|
|
9
|
+
/** Explicit target version. When set, overrides `kind`. */
|
|
10
|
+
version?: string;
|
|
11
|
+
kind?: BumpKind;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type BumpReport = {
|
|
17
|
+
root: string;
|
|
18
|
+
from: string;
|
|
19
|
+
to: string;
|
|
20
|
+
kind?: BumpKind;
|
|
21
|
+
cargo: string[];
|
|
22
|
+
npm: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_KIND: BumpKind = "patch";
|
|
26
|
+
|
|
27
|
+
export async function runBump(argv: string[]): Promise<void> {
|
|
28
|
+
const options = parseBumpArgs(argv);
|
|
29
|
+
const report = bumpWorkspace(options);
|
|
30
|
+
printReport(report, options.dryRun);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseBumpArgs(argv: string[]): BumpOptions {
|
|
34
|
+
let version: string | undefined;
|
|
35
|
+
let kind: BumpKind = DEFAULT_KIND;
|
|
36
|
+
let cwd: string | undefined;
|
|
37
|
+
let dryRun = false;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
40
|
+
const arg = argv[i];
|
|
41
|
+
if (arg === "--dry-run") {
|
|
42
|
+
dryRun = true;
|
|
43
|
+
} else if (arg === "-C" || arg === "--cwd") {
|
|
44
|
+
cwd = argv[++i];
|
|
45
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
46
|
+
version = argv[++i];
|
|
47
|
+
} else if (arg === "--patch") {
|
|
48
|
+
kind = "patch";
|
|
49
|
+
} else if (arg === "--minor") {
|
|
50
|
+
kind = "minor";
|
|
51
|
+
} else if (arg === "--major") {
|
|
52
|
+
kind = "major";
|
|
53
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
54
|
+
printBumpHelp();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
} else if (!arg.startsWith("-")) {
|
|
57
|
+
if (isBumpKind(arg)) {
|
|
58
|
+
kind = arg;
|
|
59
|
+
} else {
|
|
60
|
+
version = arg;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (version) {
|
|
66
|
+
assertVersion(version);
|
|
67
|
+
}
|
|
68
|
+
return { version, kind, cwd, dryRun };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isBumpKind(value: string): value is BumpKind {
|
|
72
|
+
return value === "patch" || value === "minor" || value === "major";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertVersion(version: string): void {
|
|
76
|
+
if (!/^\d+\.\d+\.\d+(-[\w.-]+)?$/.test(version)) {
|
|
77
|
+
throw new Error(`invalid version ${version}, expected semver like 0.0.0`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function bumpWorkspace(options: BumpOptions): BumpReport {
|
|
82
|
+
const root = findWorkspaceRoot(options.cwd ?? process.cwd());
|
|
83
|
+
const cratesDir = join(root, "projects", "crates");
|
|
84
|
+
const packagesDir = join(root, "projects", "packages");
|
|
85
|
+
|
|
86
|
+
const cargoPaths = collectCargoManifests(cratesDir);
|
|
87
|
+
const npmPaths = collectPackageManifests(packagesDir);
|
|
88
|
+
const from = readWorkspaceVersion(cargoPaths, npmPaths);
|
|
89
|
+
const to = options.version ?? bumpSemver(from, options.kind ?? DEFAULT_KIND);
|
|
90
|
+
assertVersion(to);
|
|
91
|
+
|
|
92
|
+
const cargo = cargoPaths.map((path) => bumpCargoManifest(path, to, options.dryRun));
|
|
93
|
+
const npm = npmPaths.map((path) => bumpPackageManifest(path, to, options.dryRun));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
root,
|
|
97
|
+
from,
|
|
98
|
+
to,
|
|
99
|
+
kind: options.version ? undefined : options.kind ?? DEFAULT_KIND,
|
|
100
|
+
cargo,
|
|
101
|
+
npm,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readWorkspaceVersion(cargoPaths: string[], npmPaths: string[]): string {
|
|
106
|
+
const versions = new Set<string>();
|
|
107
|
+
for (const path of cargoPaths) {
|
|
108
|
+
versions.add(readCargoVersion(path));
|
|
109
|
+
}
|
|
110
|
+
for (const path of npmPaths) {
|
|
111
|
+
versions.add(readPackageVersion(path));
|
|
112
|
+
}
|
|
113
|
+
if (versions.size === 0) {
|
|
114
|
+
throw new Error("no crate or package versions found in workspace");
|
|
115
|
+
}
|
|
116
|
+
if (versions.size > 1) {
|
|
117
|
+
throw new Error(`workspace versions are out of sync: ${[...versions].sort().join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
return [...versions][0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readCargoVersion(path: string): string {
|
|
123
|
+
const original = readFileSync(path, "utf8");
|
|
124
|
+
const match = original.match(/^version\s*=\s*"([^"]*)"/m);
|
|
125
|
+
if (!match) {
|
|
126
|
+
throw new Error(`no [package].version found in ${path}`);
|
|
127
|
+
}
|
|
128
|
+
return match[1];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readPackageVersion(path: string): string {
|
|
132
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as { version?: string };
|
|
133
|
+
if (!parsed.version) {
|
|
134
|
+
throw new Error(`no version field in ${path}`);
|
|
135
|
+
}
|
|
136
|
+
return parsed.version;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function bumpSemver(version: string, kind: BumpKind): string {
|
|
140
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-.*)?$/);
|
|
141
|
+
if (!match) {
|
|
142
|
+
throw new Error(`cannot bump non-semver version ${version}`);
|
|
143
|
+
}
|
|
144
|
+
let major = Number(match[1]);
|
|
145
|
+
let minor = Number(match[2]);
|
|
146
|
+
let patch = Number(match[3]);
|
|
147
|
+
|
|
148
|
+
switch (kind) {
|
|
149
|
+
case "patch":
|
|
150
|
+
patch += 1;
|
|
151
|
+
break;
|
|
152
|
+
case "minor":
|
|
153
|
+
minor += 1;
|
|
154
|
+
patch = 0;
|
|
155
|
+
break;
|
|
156
|
+
case "major":
|
|
157
|
+
major += 1;
|
|
158
|
+
minor = 0;
|
|
159
|
+
patch = 0;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return `${major}.${minor}.${patch}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectCargoManifests(cratesDir: string): string[] {
|
|
167
|
+
if (!existsSync(cratesDir)) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return readdirSync(cratesDir, { withFileTypes: true })
|
|
171
|
+
.filter((entry) => entry.isDirectory())
|
|
172
|
+
.map((entry) => join(cratesDir, entry.name, "Cargo.toml"))
|
|
173
|
+
.filter((path) => existsSync(path));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectPackageManifests(packagesDir: string): string[] {
|
|
177
|
+
if (!existsSync(packagesDir)) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
return readdirSync(packagesDir, { withFileTypes: true })
|
|
181
|
+
.filter((entry) => entry.isDirectory() && entry.name !== "node_modules")
|
|
182
|
+
.map((entry) => join(packagesDir, entry.name, "package.json"))
|
|
183
|
+
.filter((path) => existsSync(path));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function bumpCargoManifest(path: string, version: string, dryRun?: boolean): string {
|
|
187
|
+
const original = readFileSync(path, "utf8");
|
|
188
|
+
const next = original.replace(/^version\s*=\s*"[^"]*"/m, `version = "${version}"`);
|
|
189
|
+
if (next === original) {
|
|
190
|
+
throw new Error(`no [package].version found in ${path}`);
|
|
191
|
+
}
|
|
192
|
+
if (!dryRun) {
|
|
193
|
+
writeFileSync(path, next, "utf8");
|
|
194
|
+
}
|
|
195
|
+
return path;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function bumpPackageManifest(path: string, version: string, dryRun?: boolean): string {
|
|
199
|
+
const original = readFileSync(path, "utf8");
|
|
200
|
+
const parsed = JSON.parse(original) as { version?: string };
|
|
201
|
+
if (!parsed.version) {
|
|
202
|
+
throw new Error(`no version field in ${path}`);
|
|
203
|
+
}
|
|
204
|
+
parsed.version = version;
|
|
205
|
+
const next = `${JSON.stringify(parsed, null, 4)}\n`;
|
|
206
|
+
if (!dryRun) {
|
|
207
|
+
writeFileSync(path, next, "utf8");
|
|
208
|
+
}
|
|
209
|
+
return path;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printReport(report: BumpReport, dryRun?: boolean): void {
|
|
213
|
+
const prefix = dryRun ? "would bump" : "bumped";
|
|
214
|
+
const kind = report.kind ? ` (${report.kind})` : "";
|
|
215
|
+
console.log(`${prefix} workspace ${report.root} ${report.from} -> ${report.to}${kind}`);
|
|
216
|
+
for (const path of report.cargo) {
|
|
217
|
+
console.log(` cargo: ${path}`);
|
|
218
|
+
}
|
|
219
|
+
for (const path of report.npm) {
|
|
220
|
+
console.log(` npm: ${path}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function printBumpHelp(): void {
|
|
225
|
+
console.log(`nifty bump — align all crate and package versions in the workspace
|
|
226
|
+
|
|
227
|
+
Usage:
|
|
228
|
+
nifty bump # bump patch (default)
|
|
229
|
+
nifty bump patch
|
|
230
|
+
nifty bump minor
|
|
231
|
+
nifty bump major
|
|
232
|
+
nifty bump 1.2.3 # set explicit version
|
|
233
|
+
nifty bump --version 1.2.3
|
|
234
|
+
nifty bump --dry-run
|
|
235
|
+
nifty bump -C <cwd>
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
4
|
+
|
|
5
|
+
/** Parse shared CLI flags used across nifty commands. */
|
|
6
|
+
export function parseCliContext(argv: string[]): { cwd: string; dryRun: boolean } {
|
|
7
|
+
let cwd = process.cwd();
|
|
8
|
+
let dryRun = false;
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg === "--dry-run") {
|
|
13
|
+
dryRun = true;
|
|
14
|
+
} else if (arg === "-C" || arg === "--cwd") {
|
|
15
|
+
cwd = resolve(argv[++i] ?? cwd);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { cwd, dryRun };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Ensure `nifty.config.*` exists before running a command (skip create on `--dry-run`). */
|
|
23
|
+
export async function bootstrapNiftyConfig(argv: string[]): Promise<void> {
|
|
24
|
+
const { cwd, dryRun } = parseCliContext(argv);
|
|
25
|
+
await loadConfig({ cwd, createIfMissing: !dryRun });
|
|
26
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { runCli } from "./main.js";
|
|
2
|
+
export { bumpSemver, bumpWorkspace, runBump, type BumpKind, type BumpOptions, type BumpReport } from "./bump.js";
|
|
3
|
+
export { runPublish, type PublishOptions, type PublishReport } from "./publish.js";
|
|
4
|
+
export { runTrust, type TrustReport } from "./trust.js";
|
|
5
|
+
export { runUpdate } from "./update.js";
|
|
6
|
+
export { runLint } from "./lint.js";
|
|
7
|
+
export { runUpload } from "./upload.js";
|
|
8
|
+
export { findWorkspaceRoot, listWorkspacePackages, type WorkspacePackage } from "./workspace.js";
|
package/src/cli/lint.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { loadNiftyNative } from "../native.js";
|
|
2
|
+
|
|
3
|
+
const KNOWN_GITMOJIS = ["✨", "🎨", "🚀", "🐛", "🚑", "🔥", "💥", "♻️", "🔧", "📝", "👷", "🧹", "⬆️", "🧪", "🔨", "📦"];
|
|
4
|
+
|
|
5
|
+
type LintOptions = {
|
|
6
|
+
subjects: string[];
|
|
7
|
+
from?: string;
|
|
8
|
+
to?: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type LintDiagnostic = {
|
|
13
|
+
rule: string;
|
|
14
|
+
severity: "error" | "warning";
|
|
15
|
+
message: string;
|
|
16
|
+
subject?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function runLint(argv: string[], check: boolean): Promise<void> {
|
|
20
|
+
const options = parseLintArgs(argv);
|
|
21
|
+
const native = loadNiftyNative();
|
|
22
|
+
const subjects: string[] = [];
|
|
23
|
+
|
|
24
|
+
if (options.subjects.length > 0) {
|
|
25
|
+
subjects.push(...options.subjects);
|
|
26
|
+
} else {
|
|
27
|
+
const cwd = options.cwd ?? process.cwd();
|
|
28
|
+
const repoRoot = native.git["discover-root"](cwd);
|
|
29
|
+
const toRef = options.to ?? "HEAD";
|
|
30
|
+
const commits = native.git["collect-commits"](repoRoot, options.from, toRef);
|
|
31
|
+
subjects.push(...commits.map((commit) => commit.subject));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const diagnostics = subjects.flatMap((subject) => lintSubject(subject, native));
|
|
35
|
+
for (const item of diagnostics) {
|
|
36
|
+
const prefix = item.severity === "error" ? "error" : "warning";
|
|
37
|
+
const suffix = item.subject ? ` (${item.subject})` : "";
|
|
38
|
+
console.log(`${prefix} [${item.rule}] ${item.message}${suffix}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const errorCount = diagnostics.filter((item) => item.severity === "error").length;
|
|
42
|
+
if (check && errorCount > 0) {
|
|
43
|
+
throw new Error(`found ${errorCount} lint error(s)`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseLintArgs(argv: string[]): LintOptions {
|
|
48
|
+
const options: LintOptions = { subjects: [] };
|
|
49
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
50
|
+
const arg = argv[i];
|
|
51
|
+
if (arg === "--from") {
|
|
52
|
+
options.from = argv[++i];
|
|
53
|
+
} else if (arg === "--to") {
|
|
54
|
+
options.to = argv[++i];
|
|
55
|
+
} else if (arg === "-C" || arg === "--cwd") {
|
|
56
|
+
options.cwd = argv[++i];
|
|
57
|
+
} else if (arg === "-s" || arg === "--subject") {
|
|
58
|
+
options.subjects.push(argv[++i]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function lintSubject(subject: string, native: ReturnType<typeof loadNiftyNative>): LintDiagnostic[] {
|
|
65
|
+
const out: LintDiagnostic[] = [];
|
|
66
|
+
if (!native.gitmoji["validate-subject"](subject)) {
|
|
67
|
+
out.push({
|
|
68
|
+
rule: "gitmoji/subject",
|
|
69
|
+
severity: "error",
|
|
70
|
+
message: "commit subject must start with a known gitmoji followed by a space",
|
|
71
|
+
subject,
|
|
72
|
+
});
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const gitmoji = native.gitmoji["leading-gitmoji"](subject);
|
|
77
|
+
if (gitmoji && !KNOWN_GITMOJIS.includes(gitmoji)) {
|
|
78
|
+
out.push({
|
|
79
|
+
rule: "gitmoji/known",
|
|
80
|
+
severity: "error",
|
|
81
|
+
message: `gitmoji ${gitmoji} is not in the Nifty known gitmoji list`,
|
|
82
|
+
subject,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const body = native.gitmoji["strip-gitmoji"](subject).trim();
|
|
87
|
+
if (!body) {
|
|
88
|
+
out.push({
|
|
89
|
+
rule: "gitmoji/body",
|
|
90
|
+
severity: "warning",
|
|
91
|
+
message: "commit subject body must not be empty after the gitmoji prefix",
|
|
92
|
+
subject,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (gitmoji) {
|
|
97
|
+
const expected = native.gitmoji["format-subject"](gitmoji, body);
|
|
98
|
+
if (expected !== subject.trim()) {
|
|
99
|
+
out.push({
|
|
100
|
+
rule: "gitmoji/format",
|
|
101
|
+
severity: "warning",
|
|
102
|
+
message: `subject should be formatted as ${expected}`,
|
|
103
|
+
subject,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return out;
|
|
109
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { runBump } from "./bump.js";
|
|
2
|
+
import { bootstrapNiftyConfig } from "./context.js";
|
|
3
|
+
import { runLint } from "./lint.js";
|
|
4
|
+
import { runPublish } from "./publish.js";
|
|
5
|
+
import { runTrust } from "./trust.js";
|
|
6
|
+
import { runUpdate } from "./update.js";
|
|
7
|
+
import { runUpload } from "./upload.js";
|
|
8
|
+
|
|
9
|
+
export async function runCli(argv: string[]): Promise<number> {
|
|
10
|
+
const [command, ...rest] = argv;
|
|
11
|
+
|
|
12
|
+
if (!command || command === "-h" || command === "--help") {
|
|
13
|
+
printHelp();
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await bootstrapNiftyConfig(rest);
|
|
19
|
+
switch (command) {
|
|
20
|
+
case "update":
|
|
21
|
+
await runUpdate(rest);
|
|
22
|
+
return 0;
|
|
23
|
+
case "lint":
|
|
24
|
+
await runLint(rest, false);
|
|
25
|
+
return 0;
|
|
26
|
+
case "check":
|
|
27
|
+
await runLint(rest, true);
|
|
28
|
+
return 0;
|
|
29
|
+
case "upload":
|
|
30
|
+
await runUpload(rest);
|
|
31
|
+
return 0;
|
|
32
|
+
case "bump":
|
|
33
|
+
await runBump(rest);
|
|
34
|
+
return 0;
|
|
35
|
+
case "publish":
|
|
36
|
+
await runPublish(rest);
|
|
37
|
+
return 0;
|
|
38
|
+
case "trust":
|
|
39
|
+
await runTrust(rest);
|
|
40
|
+
return 0;
|
|
41
|
+
default:
|
|
42
|
+
console.error(`unknown command: ${command}`);
|
|
43
|
+
printHelp();
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function printHelp(): void {
|
|
53
|
+
console.log(`nifty — Nifty tooling for hybrid cargo + npm projects
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
nifty update [-i] [-C cwd]
|
|
57
|
+
nifty lint [--from ref] [--to ref] [-s subject] [-C cwd]
|
|
58
|
+
nifty check [--from ref] [--to ref] [-s subject] [-C cwd]
|
|
59
|
+
nifty upload (--release | --pages | --both) [options]
|
|
60
|
+
nifty bump [patch|minor|major|version] [--dry-run] [-C cwd]
|
|
61
|
+
nifty publish [--dry-run] [--tag <tag>] [--access public] [-C cwd]
|
|
62
|
+
nifty trust [--only <pkg>] [--dry-run] [-C cwd]
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { loadNiftyNative } from "../native.js";
|
|
2
|
+
import { authPayload, parseAuthArgs } from "./authArgs.js";
|
|
3
|
+
|
|
4
|
+
export type PublishOptions = {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
tag?: string;
|
|
8
|
+
access?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type PublishReport = {
|
|
12
|
+
root: string;
|
|
13
|
+
order: string[];
|
|
14
|
+
published: string[];
|
|
15
|
+
skipped: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function runPublish(argv: string[]): Promise<void> {
|
|
19
|
+
const options = parsePublishArgs(argv);
|
|
20
|
+
const native = loadNiftyNative();
|
|
21
|
+
const report = native.publisher["publish-workspace"]({
|
|
22
|
+
cwd: options.cwd,
|
|
23
|
+
dryRun: options.dryRun,
|
|
24
|
+
tag: options.tag,
|
|
25
|
+
access: options.access,
|
|
26
|
+
...authPayload(options.auth),
|
|
27
|
+
});
|
|
28
|
+
printReport(report, options.dryRun);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parsePublishArgs(argv: string[]) {
|
|
32
|
+
let cwd: string | undefined;
|
|
33
|
+
let dryRun = false;
|
|
34
|
+
let tag: string | undefined;
|
|
35
|
+
let access = "public";
|
|
36
|
+
const auth = parseAuthArgs(argv);
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
if (arg === "--dry-run") {
|
|
41
|
+
dryRun = true;
|
|
42
|
+
} else if (arg === "-C" || arg === "--cwd") {
|
|
43
|
+
cwd = argv[++i];
|
|
44
|
+
} else if (arg === "--tag") {
|
|
45
|
+
tag = argv[++i];
|
|
46
|
+
} else if (arg === "--access") {
|
|
47
|
+
access = argv[++i];
|
|
48
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
49
|
+
printPublishHelp();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
} else if (arg.startsWith("-")) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { cwd, dryRun, tag, access, auth };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printReport(report: PublishReport, dryRun?: boolean): void {
|
|
60
|
+
const prefix = dryRun ? "would publish" : "published";
|
|
61
|
+
console.log(`${prefix} workspace ${report.root}`);
|
|
62
|
+
if (report.skipped.length > 0) {
|
|
63
|
+
console.log(`skipped private packages: ${report.skipped.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
console.log("publish order:");
|
|
66
|
+
for (const name of report.order) {
|
|
67
|
+
const marker = report.published.includes(name) ? prefix : "pending";
|
|
68
|
+
console.log(` ${marker}: ${name}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function printPublishHelp(): void {
|
|
73
|
+
console.log(`nifty publish — publish workspace npm packages in dependency order
|
|
74
|
+
|
|
75
|
+
OTP/TOTP is generated in nifty-publisher (set NPM_TOTP_SECRET in .env.placeholder.local).
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
nifty publish
|
|
79
|
+
nifty publish --dry-run
|
|
80
|
+
nifty publish --tag next
|
|
81
|
+
nifty publish --access public
|
|
82
|
+
nifty publish --otp 123456
|
|
83
|
+
nifty publish -C <cwd>
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Resolve npm next to the active Node binary (Windows needs `npm.cmd`). */
|
|
5
|
+
export function resolveNpmExecutable(): string {
|
|
6
|
+
if (process.env.NIFTY_NPM) {
|
|
7
|
+
return process.env.NIFTY_NPM;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sibling = join(dirname(process.execPath), process.platform === "win32" ? "npm.cmd" : "npm");
|
|
11
|
+
if (existsSync(sibling)) {
|
|
12
|
+
return sibling;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
16
|
+
}
|