@doki-land/nifty 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/cli/nifty.mjs +2 -2
  2. package/package.json +8 -7
  3. package/src/cli/authArgs.ts +29 -29
  4. package/src/cli/bump-cmd.ts +15 -15
  5. package/src/cli/bump.ts +183 -94
  6. package/src/cli/change-logs-cmd.ts +26 -26
  7. package/src/cli/cli.ts +3 -3
  8. package/src/cli/commit-cmd.ts +77 -77
  9. package/src/cli/commit-lint-report.ts +44 -44
  10. package/src/cli/context.ts +9 -9
  11. package/src/cli/format-cmd.ts +6 -6
  12. package/src/cli/format.ts +15 -15
  13. package/src/cli/install-native-cmd.ts +4 -4
  14. package/src/cli/install-native.ts +24 -24
  15. package/src/cli/lint-cmd.ts +11 -11
  16. package/src/cli/lint.ts +49 -49
  17. package/src/cli/options.ts +16 -16
  18. package/src/cli/publish-cmd.ts +16 -16
  19. package/src/cli/publish.ts +38 -38
  20. package/src/cli/resolveNpm.ts +3 -3
  21. package/src/cli/retime-cmd.ts +18 -18
  22. package/src/cli/trust-cmd.ts +11 -11
  23. package/src/cli/trust.ts +26 -26
  24. package/src/cli/update-cmd.ts +5 -5
  25. package/src/cli/update.ts +12 -12
  26. package/src/cli/upload-cmd.ts +17 -17
  27. package/src/cli/upload.ts +132 -132
  28. package/src/cli/workspace.ts +49 -49
  29. package/src/config/authorMap.ts +4 -4
  30. package/src/config/defineConfig.ts +10 -8
  31. package/src/config/detectProject.ts +46 -46
  32. package/src/config/formatPresets.ts +26 -26
  33. package/src/config/loadConfig.ts +35 -35
  34. package/src/config/trustPayload.ts +7 -7
  35. package/src/config/types.ts +38 -38
  36. package/src/git.ts +14 -14
  37. package/src/github.ts +54 -54
  38. package/src/gitmoji.ts +18 -18
  39. package/src/native.ts +600 -600
  40. package/src/nifty.ts +30 -30
  41. package/src/types.ts +17 -17
package/cli/nifty.mjs CHANGED
@@ -1,11 +1,11 @@
1
- #! /usr/bin /env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { createJiti } from "jiti";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  const here = dirname(fileURLToPath(import.meta.url));
8
- const jiti = createJiti(import.meta.url, { interopDefault : true });
8
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
9
9
  const { runCli } = await jiti.import(join(here, "../src/cli/main.ts"));
10
10
 
11
11
  process.exit(await runCli(process.argv.slice(2)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doki-land/nifty",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Gitmoji-first commit conventions and release helpers (Node-API + TypeScript)",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -17,18 +17,19 @@
17
17
  "src"
18
18
  ],
19
19
  "scripts": {
20
- "typecheck": "tsc --noEmit"
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "node --test tests/*.test.mjs"
21
22
  },
22
23
  "dependencies": {
23
24
  "@vmz/commander": "^0.2.0",
24
25
  "jiti": "^2.4.2"
25
26
  },
26
27
  "optionalDependencies": {
27
- "@doki-land/nifty-win32-x64": "0.1.1",
28
- "@doki-land/nifty-linux-x64": "0.1.1",
29
- "@doki-land/nifty-linux-arm64": "0.1.1",
30
- "@doki-land/nifty-darwin-arm64": "0.1.1",
31
- "@doki-land/nifty-darwin-x64": "0.1.1"
28
+ "@doki-land/nifty-win32-x64": "0.1.3",
29
+ "@doki-land/nifty-linux-x64": "0.1.3",
30
+ "@doki-land/nifty-linux-arm64": "0.1.3",
31
+ "@doki-land/nifty-darwin-arm64": "0.1.3",
32
+ "@doki-land/nifty-darwin-x64": "0.1.3"
32
33
  },
33
34
  "publishConfig": {
34
35
  "access": "public"
@@ -1,30 +1,30 @@
1
1
  import { resolveNpmExecutable } from "./resolveNpm.js";
2
2
 
3
3
  export type AuthCliOptions = {
4
- otp ? : string;
5
- totpSecret ? : string;
6
- token ? : string;
7
- npm : string;
4
+ otp?: string;
5
+ totpSecret?: string;
6
+ token?: string;
7
+ npm: string;
8
8
  };
9
9
 
10
- export function parseAuthArgs(argv : string[]) : AuthCliOptions {
11
- let otp : string | undefined;
12
- let totpSecret : string | undefined;
13
- let token : string | undefined;
10
+ export function parseAuthArgs(argv: string[]): AuthCliOptions {
11
+ let otp: string | undefined;
12
+ let totpSecret: string | undefined;
13
+ let token: string | undefined;
14
14
 
15
- for(let i = 0; i<argv.length; i += 1){
15
+ for (let i = 0; i < argv.length; i += 1) {
16
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=")){
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
24
  otp = arg.slice("--otp=".length);
25
- } else if(arg.startsWith("--totp-secret=")){
25
+ } else if (arg.startsWith("--totp-secret=")) {
26
26
  totpSecret = arg.slice("--totp-secret=".length);
27
- } else if(arg.startsWith("--token=")){
27
+ } else if (arg.startsWith("--token=")) {
28
28
  token = arg.slice("--token=".length);
29
29
  }
30
30
  }
@@ -33,20 +33,20 @@ export function parseAuthArgs(argv : string[]) : AuthCliOptions {
33
33
  otp,
34
34
  totpSecret,
35
35
  token,
36
- npm : resolveNpmExecutable(),
36
+ npm: resolveNpmExecutable(),
37
37
  };
38
38
  }
39
39
 
40
- export function authPayload(options : AuthCliOptions) : {
41
- otp ? : string;
42
- totpSecret ? : string;
43
- token ? : string;
44
- npm : string;
45
- }{
40
+ export function authPayload(options: AuthCliOptions): {
41
+ otp?: string;
42
+ totpSecret?: string;
43
+ token?: string;
44
+ npm: string;
45
+ } {
46
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,
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
51
  };
52
52
  }
@@ -4,17 +4,17 @@ import { bumpWorkspace, printBumpReport, type BumpKind, type BumpOptions } from
4
4
  import { bootstrapFromOptions } from "./context.js";
5
5
  import { cwdFrom, flag, str } from "./options.js";
6
6
 
7
- export function registerBumpCommand(cli : Cli) : void {
7
+ export function registerBumpCommand(cli: Cli): void {
8
8
  cli.command("bump", "cli.cmd.bump")
9
9
  .option("--dry-run", "cli.opt.dry-run")
10
10
  .option("--version, -v <version>", "cli.opt.version")
11
11
  .option("--patch", "cli.opt.patch")
12
12
  .option("--minor", "cli.opt.minor")
13
13
  .option("--major", "cli.opt.major")
14
- .action(async(options) =>cmdBump(options));
14
+ .action(async (options) => cmdBump(options));
15
15
  }
16
16
 
17
- export async function cmdBump(options : ParsedOptions) : Promise<number>{
17
+ export async function cmdBump(options: ParsedOptions): Promise<number> {
18
18
  await bootstrapFromOptions(options);
19
19
  const bumpOptions = bumpOptionsFromParsed(options);
20
20
  const report = bumpWorkspace(bumpOptions);
@@ -22,34 +22,34 @@ export async function cmdBump(options : ParsedOptions) : Promise<number>{
22
22
  return 0;
23
23
  }
24
24
 
25
- function bumpOptionsFromParsed(options : ParsedOptions) : BumpOptions {
25
+ function bumpOptionsFromParsed(options: ParsedOptions): BumpOptions {
26
26
  let version = str(options, "version");
27
- let kind : BumpKind = "patch";
28
- if(flag(options, "major")){
27
+ let kind: BumpKind = "patch";
28
+ if (flag(options, "major")) {
29
29
  kind = "major";
30
- } else if(flag(options, "minor")){
30
+ } else if (flag(options, "minor")) {
31
31
  kind = "minor";
32
- } else if(flag(options, "patch")){
32
+ } else if (flag(options, "patch")) {
33
33
  kind = "patch";
34
34
  }
35
- for(const positional of options._){
36
- if(isBumpKind(positional)){
35
+ for (const positional of options._) {
36
+ if (isBumpKind(positional)) {
37
37
  kind = positional;
38
- } else if(positional){
38
+ } else if (positional) {
39
39
  version = positional;
40
40
  }
41
41
  }
42
- if(version && ! /^\d+\.\d+\.\d+(-[\w.-]+)?$/ .test(version)){
42
+ if (version && !/^\d+\.\d+\.\d+(-[\w.-]+)?$/.test(version)) {
43
43
  throw new Error(`invalid version ${version}, expected semver like 0.0.0`);
44
44
  }
45
45
  return {
46
46
  version,
47
47
  kind,
48
- cwd : cwdFrom(options),
49
- dryRun : flag(options, "dry-run"),
48
+ cwd: cwdFrom(options),
49
+ dryRun: flag(options, "dry-run"),
50
50
  };
51
51
  }
52
52
 
53
- function isBumpKind(value : string) : value is BumpKind {
53
+ function isBumpKind(value: string): value is BumpKind {
54
54
  return value === "patch" || value === "minor" || value === "major";
55
55
  }
package/src/cli/bump.ts CHANGED
@@ -7,54 +7,54 @@ export type BumpKind = "patch" | "minor" | "major";
7
7
 
8
8
  export type BumpOptions = {
9
9
  /** Explicit target version. When set, overrides `kind`. */
10
- version ? : string;
11
- kind ? : BumpKind;
12
- cwd ? : string;
13
- dryRun ? : boolean;
10
+ version?: string;
11
+ kind?: BumpKind;
12
+ cwd?: string;
13
+ dryRun?: boolean;
14
14
  };
15
15
 
16
16
  export type BumpReport = {
17
- root : string;
18
- from : string;
19
- to : string;
20
- kind ? : BumpKind;
21
- cargo : string[];
22
- npm : string[];
17
+ root: string;
18
+ from: string;
19
+ to: string;
20
+ kind?: BumpKind;
21
+ cargo: string[];
22
+ npm: string[];
23
23
  };
24
24
 
25
- const DEFAULT_KIND : BumpKind = "patch";
25
+ const DEFAULT_KIND: BumpKind = "patch";
26
26
 
27
- export async function runBump(argv : string[]) : Promise<void>{
27
+ export async function runBump(argv: string[]): Promise<void> {
28
28
  const options = parseBumpArgs(argv);
29
29
  const report = bumpWorkspace(options);
30
30
  printBumpReport(report, options.dryRun);
31
31
  }
32
32
 
33
- function parseBumpArgs(argv : string[]) : BumpOptions {
34
- let version : string | undefined;
35
- let kind : BumpKind = DEFAULT_KIND;
36
- let cwd : string | undefined;
33
+ function parseBumpArgs(argv: string[]): BumpOptions {
34
+ let version: string | undefined;
35
+ let kind: BumpKind = DEFAULT_KIND;
36
+ let cwd: string | undefined;
37
37
  let dryRun = false;
38
38
 
39
- for(let i = 0; i<argv.length; i += 1){
39
+ for (let i = 0; i < argv.length; i += 1) {
40
40
  const arg = argv[i];
41
- if(arg === "--dry-run"){
41
+ if (arg === "--dry-run") {
42
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"){
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
48
  kind = "patch";
49
- } else if(arg === "--minor"){
49
+ } else if (arg === "--minor") {
50
50
  kind = "minor";
51
- } else if(arg === "--major"){
51
+ } else if (arg === "--major") {
52
52
  kind = "major";
53
- } else if(arg === "-h" || arg === "--help"){
53
+ } else if (arg === "-h" || arg === "--help") {
54
54
  printBumpHelp();
55
55
  process.exit(0);
56
- } else if(!arg.startsWith("-")){
57
- if(isBumpKind(arg)){
56
+ } else if (!arg.startsWith("-")) {
57
+ if (isBumpKind(arg)) {
58
58
  kind = arg;
59
59
  } else {
60
60
  version = arg;
@@ -62,98 +62,112 @@ function parseBumpArgs(argv : string[]) : BumpOptions {
62
62
  }
63
63
  }
64
64
 
65
- if(version){
65
+ if (version) {
66
66
  assertVersion(version);
67
67
  }
68
68
  return { version, kind, cwd, dryRun };
69
69
  }
70
70
 
71
- function isBumpKind(value : string) : value is BumpKind {
71
+ function isBumpKind(value: string): value is BumpKind {
72
72
  return value === "patch" || value === "minor" || value === "major";
73
73
  }
74
74
 
75
- function assertVersion(version : string) : void {
76
- if(! /^\d+\.\d+\.\d+(-[\w.-]+)?$/ .test(version)){
75
+ function assertVersion(version: string): void {
76
+ if (!/^\d+\.\d+\.\d+(-[\w.-]+)?$/.test(version)) {
77
77
  throw new Error(`invalid version ${version}, expected semver like 0.0.0`);
78
78
  }
79
79
  }
80
80
 
81
- export function bumpWorkspace(options : BumpOptions) : BumpReport {
81
+ export function bumpWorkspace(options: BumpOptions): BumpReport {
82
82
  const root = findWorkspaceRoot(options.cwd ?? process.cwd());
83
83
  const cratesDir = join(root, "projects", "crates");
84
84
  const packagesDir = join(root, "projects", "packages");
85
85
 
86
86
  const cargoPaths = collectCargoManifests(cratesDir);
87
87
  const npmPaths = collectPackageManifests(packagesDir);
88
- const from = readWorkspaceVersion(cargoPaths, npmPaths);
88
+ const from = readWorkspaceVersion(root, cargoPaths, npmPaths);
89
89
  const to = options.version ?? bumpSemver(from, options.kind ?? DEFAULT_KIND);
90
90
  assertVersion(to);
91
91
 
92
- const cargo = cargoPaths.map((path) =>bumpCargoManifest(path, to, options.dryRun));
93
- const npm = npmPaths.map((path) =>bumpPackageManifest(path, to, options.dryRun));
92
+ const cargoUpdates = prepareCargoUpdates(root, cargoPaths, to);
93
+ const npmUpdates = npmPaths.map((path) => preparePackageUpdate(path, to));
94
+ if (!options.dryRun) {
95
+ cargoUpdates.concat(npmUpdates).forEach((update) => writeFileSync(update.path, update.next, "utf8"));
96
+ }
94
97
 
95
98
  return {
96
99
  root,
97
100
  from,
98
101
  to,
99
- kind : options.version ? undefined : options.kind ?? DEFAULT_KIND,
100
- cargo,
101
- npm,
102
+ kind: options.version ? undefined : options.kind ?? DEFAULT_KIND,
103
+ cargo: cargoUpdates.map((update) => update.path),
104
+ npm: npmUpdates.map((update) => update.path),
102
105
  };
103
106
  }
104
107
 
105
- function readWorkspaceVersion(cargoPaths : string[], npmPaths : string[]) : string {
108
+ function readWorkspaceVersion(root: string, cargoPaths: string[], npmPaths: string[]): string {
106
109
  const versions = new Set<string>();
107
- for(const path of cargoPaths){
108
- versions.add(readCargoVersion(path));
110
+ let usesWorkspaceVersion = false;
111
+ for (const path of cargoPaths) {
112
+ const info = readCargoVersion(path);
113
+ if (info.inherited) usesWorkspaceVersion = true;
114
+ else versions.add(info.version);
115
+ }
116
+ if (usesWorkspaceVersion) {
117
+ versions.add(readWorkspacePackageVersion(join(root, "Cargo.toml")).version);
109
118
  }
110
- for(const path of npmPaths){
119
+ for (const path of npmPaths) {
111
120
  versions.add(readPackageVersion(path));
112
121
  }
113
- if(versions.size === 0){
122
+ if (versions.size === 0) {
114
123
  throw new Error("no crate or package versions found in workspace");
115
124
  }
116
- if(versions.size>1){
125
+ if (versions.size > 1) {
117
126
  throw new Error(`workspace versions are out of sync: ${[...versions].sort().join(", ")}`);
118
127
  }
119
- return[...versions][0];
128
+ return [...versions][0];
120
129
  }
121
130
 
122
- function readCargoVersion(path : string) : string {
131
+ type CargoVersion = { version: string; inherited: boolean };
132
+
133
+ type FileUpdate = { path: string; next: string };
134
+
135
+ function readCargoVersion(path: string): CargoVersion {
123
136
  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}`);
137
+ const field = findTomlField(original, "package", "version.workspace") ?? findTomlField(original, "package", "version");
138
+ if (!field) throw new Error(`no [package].version found in ${path}`);
139
+ if (isWorkspaceInheritedVersion(field)) {
140
+ return { version: "", inherited: true };
127
141
  }
128
- return match[1];
142
+ return { version: parseTomlString(field.value, `[package].version in ${path}`), inherited: false };
129
143
  }
130
144
 
131
- function readPackageVersion(path : string) : string {
132
- const parsed = JSON.parse(readFileSync(path, "utf8"))as { version ? : string };
133
- if(!parsed.version){
145
+ function readPackageVersion(path: string): string {
146
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as { version?: string };
147
+ if (!parsed.version) {
134
148
  throw new Error(`no version field in ${path}`);
135
149
  }
136
150
  return parsed.version;
137
151
  }
138
152
 
139
- export function bumpSemver(version : string, kind : BumpKind) : string {
140
- const match = version.match( /^(\d+)\.(\d+)\.(\d+)(?:-.*)?$/ );
141
- if(!match){
153
+ export function bumpSemver(version: string, kind: BumpKind): string {
154
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-.*)?$/);
155
+ if (!match) {
142
156
  throw new Error(`cannot bump non-semver version ${version}`);
143
157
  }
144
158
  let major = Number(match[1]);
145
159
  let minor = Number(match[2]);
146
160
  let patch = Number(match[3]);
147
161
 
148
- switch(kind){
149
- case "patch" :
162
+ switch (kind) {
163
+ case "patch":
150
164
  patch += 1;
151
165
  break;
152
- case "minor" :
166
+ case "minor":
153
167
  minor += 1;
154
168
  patch = 0;
155
169
  break;
156
- case "major" :
170
+ case "major":
157
171
  major += 1;
158
172
  minor = 0;
159
173
  patch = 0;
@@ -163,65 +177,138 @@ export function bumpSemver(version : string, kind : BumpKind) : string {
163
177
  return `${major}.${minor}.${patch}`;
164
178
  }
165
179
 
166
- function collectCargoManifests(cratesDir : string) : string[]{
167
- if(!existsSync(cratesDir)){
168
- return[];
180
+ function collectCargoManifests(cratesDir: string): string[] {
181
+ if (!existsSync(cratesDir)) {
182
+ return [];
169
183
  }
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));
184
+ return readdirSync(cratesDir, { withFileTypes: true })
185
+ .filter((entry) => entry.isDirectory())
186
+ .map((entry) => join(cratesDir, entry.name, "Cargo.toml"))
187
+ .filter((path) => existsSync(path));
174
188
  }
175
189
 
176
- function collectPackageManifests(packagesDir : string) : string[]{
177
- if(!existsSync(packagesDir)){
178
- return[];
190
+ function collectPackageManifests(packagesDir: string): string[] {
191
+ if (!existsSync(packagesDir)) {
192
+ return [];
179
193
  }
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));
194
+ return readdirSync(packagesDir, { withFileTypes: true })
195
+ .filter((entry) => entry.isDirectory() && entry.name !== "node_modules")
196
+ .map((entry) => join(packagesDir, entry.name, "package.json"))
197
+ .filter((path) => existsSync(path));
184
198
  }
185
199
 
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}`);
200
+ function prepareCargoUpdates(root: string, cargoPaths: string[], version: string): FileUpdate[] {
201
+ const updates: FileUpdate[] = [];
202
+ let usesWorkspaceVersion = false;
203
+ for (const path of cargoPaths) {
204
+ const original = readFileSync(path, "utf8");
205
+ const field = findTomlField(original, "package", "version.workspace") ?? findTomlField(original, "package", "version");
206
+ if (!field) throw new Error(`no [package].version found in ${path}`);
207
+ if (isWorkspaceInheritedVersion(field)) {
208
+ usesWorkspaceVersion = true;
209
+ continue;
210
+ }
211
+ parseTomlString(field.value, `[package].version in ${path}`);
212
+ updates.push({ path, next: replaceTomlField(original, field, version) });
191
213
  }
192
- if(!dryRun){
193
- writeFileSync(path, next, "utf8");
214
+ if (usesWorkspaceVersion) {
215
+ const path = join(root, "Cargo.toml");
216
+ const original = readFileSync(path, "utf8");
217
+ const field = findTomlField(original, "workspace.package", "version");
218
+ if (!field) {
219
+ throw new Error(`Cargo members inherit version.workspace but ${path} has no [workspace.package].version`);
220
+ }
221
+ parseTomlString(field.value, `[workspace.package].version in ${path}`);
222
+ updates.push({ path, next: replaceTomlField(original, field, version) });
194
223
  }
195
- return path;
224
+ return updates;
196
225
  }
197
226
 
198
- function bumpPackageManifest(path : string, version : string, dryRun ? : boolean) : string {
227
+ function readWorkspacePackageVersion(path: string): CargoVersion {
199
228
  const original = readFileSync(path, "utf8");
200
- const parsed = JSON.parse(original)as { version ? : string };
201
- if(!parsed.version){
229
+ const field = findTomlField(original, "workspace.package", "version");
230
+ if (!field) throw new Error(`Cargo members inherit version.workspace but ${path} has no [workspace.package].version`);
231
+ return { version: parseTomlString(field.value, `[workspace.package].version in ${path}`), inherited: false };
232
+ }
233
+
234
+ function isWorkspaceInheritedVersion(field: NonNullable<ReturnType<typeof findTomlField>>): boolean {
235
+ if (field.key === "version.workspace") return true;
236
+ const value = field.value.trim();
237
+ if (!value.startsWith("{") || !value.endsWith("}")) return false;
238
+ return value
239
+ .slice(1, -1)
240
+ .split(",")
241
+ .some((assignment) => {
242
+ const equals = assignment.indexOf("=");
243
+ return equals >= 0 && assignment.slice(0, equals).trim() === "workspace" && assignment.slice(equals + 1).trim() === "true";
244
+ });
245
+ }
246
+
247
+ function findTomlField(contents: string, table: string, name: string) {
248
+ let currentTable = "";
249
+ const lines = contents.split(/(?<=\n)/);
250
+ for (let index = 0; index < lines.length; index += 1) {
251
+ const line = lines[index];
252
+ const trimmed = line.trim();
253
+ const tableMatch = trimmed.match(/^\[([^\]]+)\]$/);
254
+ if (tableMatch) {
255
+ currentTable = tableMatch[1].trim();
256
+ continue;
257
+ }
258
+ if (trimmed.startsWith("[[")) {
259
+ currentTable = "";
260
+ continue;
261
+ }
262
+ if (currentTable !== table) continue;
263
+ const fieldMatch = line.match(new RegExp(`^(\\s*)(${name.replace(".", "\\.")})\\s*=\\s*(.*?)(\\r?\\n?)$`));
264
+ if (fieldMatch) return { index, line, indent: fieldMatch[1], key: fieldMatch[2], value: fieldMatch[3], ending: fieldMatch[4] };
265
+ }
266
+ return undefined;
267
+ }
268
+
269
+ function parseTomlString(value: string, description: string): string {
270
+ const trimmed = value.trim();
271
+ const quote = trimmed[0];
272
+ if (quote !== '"' && quote !== "'") throw new Error(`expected a quoted string for ${description}`);
273
+ const closingQuote = trimmed.indexOf(quote, 1);
274
+ if (closingQuote < 0) throw new Error(`expected a quoted string for ${description}`);
275
+ const trailing = trimmed.slice(closingQuote + 1).trim();
276
+ if (trailing && !trailing.startsWith("#")) throw new Error(`expected a quoted string for ${description}`);
277
+ return trimmed.slice(1, closingQuote);
278
+ }
279
+
280
+ function replaceTomlField(contents: string, field: NonNullable<ReturnType<typeof findTomlField>>, version: string): string {
281
+ const value = field.value.trimStart();
282
+ const closingQuote = value.indexOf(value[0], 1);
283
+ const suffix = closingQuote >= 0 ? value.slice(closingQuote + 1) : "";
284
+ const line = `${field.indent}${field.key} = "${version}"${suffix}${field.ending}`;
285
+ return contents.split(/(?<=\n)/).map((current, index) => index === field.index ? line : current).join("");
286
+ }
287
+
288
+ function preparePackageUpdate(path: string, version: string): FileUpdate {
289
+ const original = readFileSync(path, "utf8");
290
+ const parsed = JSON.parse(original) as { version?: string };
291
+ if (!parsed.version) {
202
292
  throw new Error(`no version field in ${path}`);
203
293
  }
204
294
  parsed.version = version;
205
295
  const next = `${JSON.stringify(parsed, null, 4)}\n`;
206
- if(!dryRun){
207
- writeFileSync(path, next, "utf8");
208
- }
209
- return path;
296
+ return { path, next };
210
297
  }
211
298
 
212
- export function printBumpReport(report : BumpReport, dryRun ? : boolean) : void {
299
+ export function printBumpReport(report: BumpReport, dryRun?: boolean): void {
213
300
  const prefix = dryRun ? "would bump" : "bumped";
214
301
  const kind = report.kind ? ` (${report.kind})` : "";
215
302
  console.log(`${prefix} workspace ${report.root} ${report.from} -> ${report.to}${kind}`);
216
- for(const path of report.cargo){
303
+ for (const path of report.cargo) {
217
304
  console.log(` cargo: ${path}`);
218
305
  }
219
- for(const path of report.npm){
306
+ for (const path of report.npm) {
220
307
  console.log(` npm: ${path}`);
221
308
  }
222
309
  }
223
310
 
224
- function printBumpHelp() : void {
311
+ function printBumpHelp(): void {
225
312
  console.log(`nifty bump — align all crate and package versions in the workspace
226
313
 
227
314
  Usage:
@@ -233,5 +320,7 @@ Usage:
233
320
  nifty bump --version 1.2.3
234
321
  nifty bump --dry-run
235
322
  nifty bump -C <cwd>
323
+
324
+ Cargo members using version.workspace = true are updated through the root [workspace.package].version.
236
325
  `);
237
326
  }