@smoothbricks/cli 0.3.0 → 0.3.2

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 CHANGED
@@ -80,7 +80,8 @@ Packages with internal workspace dependencies also need Nx-aware scripts so depe
80
80
  For safe build, test, typecheck, benchmark, dev, and preview commands, `smoo monorepo validate --fix` moves the real
81
81
  command into `package.json` `nx.targets.<target>.options.command`, sets `cwd` to `{projectRoot}`, and replaces the
82
82
  script with an `nx run <project>:<target>` alias. Continuous commands such as `astro dev`, `vite dev`, and previews get
83
- `--tui=false --outputStyle=stream` on the alias and `continuous: true` on the Nx target. Simple leading environment
83
+ an explicit output style on the alias and `continuous: true` on the Nx target. Astro/Vite dev servers use
84
+ `--outputStyle=dynamic-legacy`; other continuous targets use `--outputStyle=stream`. Simple leading environment
84
85
  assignments are moved into `nx.targets.<target>.options.env` so commands such as
85
86
  `NODE_OPTIONS='--import=extensionless/register' astro dev` remain shell-independent.
86
87
 
@@ -1 +1 @@
1
- {"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,iBAAiB,GAAG,SAAS,GAAG,YAAY,CAAC;CACxG;AAwED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,EAAE,CAG/F;AA0ED,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD"}
1
+ {"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,iBAAiB,GAAG,SAAS,GAAG,YAAY,CAAC;CACxG;AA8ED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,EAAE,CAG/F;AA0ED,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD"}
@@ -4,6 +4,12 @@ import { fileURLToPath } from 'node:url';
4
4
  import { listReleasePackages, readPackageJson } from '../lib/workspace.js';
5
5
  import { renderPublishWorkflowYaml } from './publish-workflow.js';
6
6
  const managedFiles = [
7
+ {
8
+ kind: 'raw',
9
+ source: 'tooling/direnv/repo-path',
10
+ target: 'tooling/direnv/repo-path',
11
+ executable: true,
12
+ },
7
13
  {
8
14
  kind: 'raw',
9
15
  source: 'tooling/direnv/github-actions-bootstrap.sh',
@@ -1,6 +1,7 @@
1
1
  import type { ReleasePackageInfo } from './core.js';
2
2
  export interface AutoReleaseCandidateShell {
3
3
  gitRefExists(ref: string): Promise<boolean>;
4
+ latestStableReleaseRef(projectName: string): Promise<string | null>;
4
5
  packageChangedFilesSince(ref: string, packagePath: string): Promise<string[]>;
5
6
  packageJsonAtRef(ref: string, packagePath: string): Promise<Record<string, unknown> | null>;
6
7
  currentPackageJson(packagePath: string): Promise<Record<string, unknown> | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"candidates.d.ts","sourceRoot":"","sources":["../../src/release/candidates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGpD,MAAM,WAAW,yBAAyB;IACxC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9E,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5F,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACjF,yBAAyB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvF,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAgCD,wBAAsB,4BAA4B,CAAC,OAAO,SAAS,kBAAkB,EACnF,KAAK,EAAE,yBAAyB,EAChC,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,OAAO,EAAE,CAAC,CAQpB"}
1
+ {"version":3,"file":"candidates.d.ts","sourceRoot":"","sources":["../../src/release/candidates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAGpD,MAAM,WAAW,yBAAyB;IACxC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACpE,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9E,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5F,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACjF,yBAAyB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvF,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAgCD,wBAAsB,4BAA4B,CAAC,OAAO,SAAS,kBAAkB,EACnF,KAAK,EAAE,yBAAyB,EAChC,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,OAAO,EAAE,CAAC,CAQpB"}
@@ -43,7 +43,19 @@ async function isAutoReleaseCandidate(shell, pkg) {
43
43
  if (await shell.gitRefExists(tagRef)) {
44
44
  return packageHasReleasableChangesSince(shell, tagRef, pkg);
45
45
  }
46
- return !pkg.version.includes('-') && shell.packageHasHistory(pkg.path);
46
+ if (pkg.version.includes('-')) {
47
+ // Prerelease version (e.g. 1.0.1-next.0) without its own tag: check for
48
+ // releasable changes since the last stable release. The prerelease bump is
49
+ // added after every release to make it obvious during development that the
50
+ // working version is unreleased — it should not suppress the next release.
51
+ const stableRef = await shell.latestStableReleaseRef(pkg.projectName);
52
+ if (stableRef) {
53
+ return packageHasReleasableChangesSince(shell, stableRef, pkg);
54
+ }
55
+ // No prior stable release — first release must be intentional, not auto.
56
+ return false;
57
+ }
58
+ return shell.packageHasHistory(pkg.path);
47
59
  }
48
60
  async function packageHasReleasableChangesSince(shell, ref, pkg) {
49
61
  const changedFiles = await shell.packageChangedFilesSince(ref, pkg.path);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/release/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/release/index.ts"],"names":[],"mappings":"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"}
@@ -337,6 +337,7 @@ async function releaseVersionPackages(root, packages, bump) {
337
337
  }
338
338
  return autoReleaseCandidatePackages({
339
339
  gitRefExists: (ref) => gitRefExists(root, ref),
340
+ latestStableReleaseRef: (projectName) => latestStableReleaseRef(root, projectName),
340
341
  packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
341
342
  packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
342
343
  currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
@@ -795,6 +796,27 @@ async function gitRefExists(root, ref) {
795
796
  const result = await $ `git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
796
797
  return result.exitCode === 0;
797
798
  }
799
+ async function latestStableReleaseRef(root, projectName) {
800
+ // List tags matching <projectName>@* sorted newest-first by version, then
801
+ // return the first non-prerelease tag as a full ref.
802
+ const pattern = `${projectName}@*`;
803
+ const result = await $ `git tag --list ${pattern} --sort=-v:refname`.cwd(root).quiet().nothrow();
804
+ if (result.exitCode !== 0) {
805
+ return null;
806
+ }
807
+ const tags = decode(result.stdout)
808
+ .split('\n')
809
+ .map((t) => t.trim())
810
+ .filter(Boolean);
811
+ const prefix = `${projectName}@`;
812
+ for (const tagName of tags) {
813
+ const version = tagName.slice(prefix.length);
814
+ if (version && !version.includes('-')) {
815
+ return `refs/tags/${tagName}`;
816
+ }
817
+ }
818
+ return null;
819
+ }
798
820
  async function packageChangedFilesSince(root, ref, packagePath) {
799
821
  const result = await $ `git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
800
822
  if (result.exitCode !== 0) {
@@ -14,11 +14,7 @@ clear_devenv_cache_state() {
14
14
  }
15
15
 
16
16
  add_repo_paths() {
17
- {
18
- echo "$repo_root/tooling/direnv/.devenv/profile/bin"
19
- echo "$repo_root/node_modules/.bin"
20
- echo "$repo_root/tooling"
21
- } >> "$GITHUB_PATH"
17
+ "$repo_root/tooling/direnv/repo-path" --github-path
22
18
  }
23
19
 
24
20
  restore_nix_store() {
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Single source of truth for repo-local PATH entries.
3
+ # All tooling that sets up PATH must call this instead of hardcoding paths.
4
+ #
5
+ # Usage:
6
+ # export PATH="$(tooling/direnv/repo-path)" # shell
7
+ # tooling/direnv/repo-path --github-path # CI — appends to $GITHUB_PATH
8
+ # tooling/direnv/repo-path --print # debug — one entry per line
9
+ set -euo pipefail
10
+
11
+ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12
+
13
+ # Order: most-specific → least-specific.
14
+ entries=(
15
+ "$root/tooling/direnv/.devenv/profile/bin"
16
+ "$root/tooling/git-hooks"
17
+ "$root/tooling"
18
+ "$root/tooling/node_modules/.bin"
19
+ "$root/node_modules/.bin"
20
+ )
21
+
22
+ case "${1:-}" in
23
+ --github-path)
24
+ # GITHUB_PATH prepends entries; last line written = first in PATH.
25
+ # Reverse so most-specific directories are searched first.
26
+ for (( i=${#entries[@]}-1; i>=0; i-- )); do
27
+ echo "${entries[i]}"
28
+ done >> "$GITHUB_PATH" ;;
29
+ --print)
30
+ printf '%s\n' "${entries[@]}" ;;
31
+ *)
32
+ IFS=:
33
+ echo "${entries[*]}:$PATH" ;;
34
+ esac
@@ -2,7 +2,7 @@
2
2
  cd "$(git rev-parse --show-toplevel)"
3
3
  TOOLING="$PWD/tooling"
4
4
 
5
- export PATH="$TOOLING/git-hooks:$TOOLING:$PWD/node_modules/.bin:$TOOLING/direnv/.devenv/profile/bin:$PATH"
5
+ export PATH="$("$TOOLING/direnv/repo-path")"
6
6
 
7
7
  set -e -o pipefail
8
8
 
@@ -2,7 +2,7 @@
2
2
  cd "$(git rev-parse --show-toplevel)"
3
3
  TOOLING="$PWD/tooling"
4
4
 
5
- export PATH="$TOOLING/git-hooks:$TOOLING:$PWD/node_modules/.bin:$TOOLING/direnv/.devenv/profile/bin:$PATH"
5
+ export PATH="$("$TOOLING/direnv/repo-path")"
6
6
 
7
7
  set -e -o pipefail
8
8
 
@@ -20,9 +20,9 @@ runs:
20
20
  working-directory: tooling/direnv
21
21
  # build-shell normally adds repo paths to GITHUB_PATH, but this cleanup
22
22
  # must still run when build-shell fails before that happens.
23
- run:
24
- PATH="$PWD/.devenv/profile/bin:$PWD/../../node_modules/.bin:$PWD/../../tooling:$PATH" ../../tooling/smoo
25
- github-ci cleanup-cache
23
+ run: |
24
+ export PATH="$(../../tooling/direnv/repo-path)"
25
+ smoo github-ci cleanup-cache
26
26
 
27
27
  - name: 🧊 Save Nix profiles and store NAR
28
28
  if: ${{ steps.cleanup-cache.outputs.cache-ready == 'true' && inputs.nix-cache-hit != 'true' }}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
7
7
  "smoo": "./bin/smoo"
8
8
  },
9
9
  "scripts": {
10
- "test": "nx run cli:test --tui=false --outputStyle=stream"
10
+ "test": "nx run cli:test --outputStyle=stream"
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "module": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@arethetypeswrong/cli": "^0.18.2",
48
- "@smoothbricks/nx-plugin": "0.2.0",
48
+ "@smoothbricks/nx-plugin": "0.2.2-next.0",
49
49
  "@smoothbricks/validation": "0.1.1",
50
50
  "commander": "^14.0.3",
51
51
  "publint": "^0.3.18",
@@ -26,6 +26,12 @@ interface ManagedFileContext {
26
26
  }
27
27
 
28
28
  const managedFiles: ManagedFile[] = [
29
+ {
30
+ kind: 'raw',
31
+ source: 'tooling/direnv/repo-path',
32
+ target: 'tooling/direnv/repo-path',
33
+ executable: true,
34
+ },
29
35
  {
30
36
  kind: 'raw',
31
37
  source: 'tooling/direnv/github-actions-bootstrap.sh',
@@ -603,7 +603,7 @@ describe('workspace package script policy', () => {
603
603
  },
604
604
  },
605
605
  },
606
- scripts: { test: 'nx run native:test --tui=false --outputStyle=stream' },
606
+ scripts: { test: 'nx run native:test --outputStyle=stream' },
607
607
  },
608
608
  ],
609
609
  });
@@ -800,9 +800,9 @@ describe('workspace package script policy', () => {
800
800
  const app = await readJson(join(root, 'packages/app/package.json'));
801
801
  expect(app.dependencies).toEqual({ '@smoothbricks/lib': 'workspace:*' });
802
802
  expect(app.scripts).toEqual({
803
- dev: 'nx run app:dev --tui=false --outputStyle=stream',
804
- serve: 'nx run app:serve --tui=false --outputStyle=stream',
805
- test: 'nx run app:test --tui=false --outputStyle=stream',
803
+ dev: 'nx run app:dev --outputStyle=dynamic-legacy',
804
+ serve: 'nx run app:serve --outputStyle=dynamic-legacy',
805
+ test: 'nx run app:test --outputStyle=stream',
806
806
  build: 'nx run app:build',
807
807
  deploy: 'wrangler deploy',
808
808
  astro: 'astro',
@@ -853,7 +853,7 @@ describe('workspace package script policy', () => {
853
853
  dir: 'app',
854
854
  name: '@smoothbricks/app',
855
855
  dependencies: { '@smoothbricks/lib': 'workspace:*' },
856
- scripts: { test: 'nx run app:test --tui=false --outputStyle=stream' },
856
+ scripts: { test: 'nx run app:test --outputStyle=stream' },
857
857
  nx: {
858
858
  name: 'app',
859
859
  targets: {
@@ -902,9 +902,9 @@ describe('workspace package script policy', () => {
902
902
 
903
903
  const website = await readJson(join(root, 'packages/website/package.json'));
904
904
  expect(website.scripts).toEqual({
905
- dev: 'nx run website:dev --tui=false --outputStyle=stream',
905
+ dev: 'nx run website:dev --outputStyle=dynamic-legacy',
906
906
  build: 'nx run website:build',
907
- preview: 'nx run website:preview --tui=false --outputStyle=stream',
907
+ preview: 'nx run website:preview --outputStyle=stream',
908
908
  astro: 'astro',
909
909
  });
910
910
  expect(website.nx).toEqual({
@@ -963,7 +963,7 @@ describe('workspace package script policy', () => {
963
963
  applyWorkspaceDependencyDefaults(root);
964
964
 
965
965
  const app = await readJson(join(root, 'packages/app/package.json'));
966
- expect(app.scripts).toEqual({ test: 'nx run @external/app:test --tui=false --outputStyle=stream' });
966
+ expect(app.scripts).toEqual({ test: 'nx run @external/app:test --outputStyle=stream' });
967
967
  expect(app.nx).toEqual({
968
968
  name: '@external/app',
969
969
  targets: {
@@ -175,7 +175,7 @@ describe('auto release candidate filtering', () => {
175
175
  });
176
176
  });
177
177
 
178
- it('does not auto-release an untagged next prerelease package version', async () => {
178
+ it('does not auto-release an untagged next prerelease package version with no prior stable release', async () => {
179
179
  await withFixtureRepo(async (root) => {
180
180
  await writePackage(root, next.name, next.path, next.version);
181
181
  await git(root, ['add', '.']);
@@ -185,6 +185,46 @@ describe('auto release candidate filtering', () => {
185
185
  });
186
186
  });
187
187
 
188
+ it('selects a prerelease package when releasable changes exist since the last stable tag', async () => {
189
+ await withFixtureRepo(async (root) => {
190
+ await writePackage(root, next.name, next.path, '1.0.0');
191
+ await git(root, ['add', '.']);
192
+ await git(root, ['commit', '-m', 'chore(release): publish']);
193
+ await tag(root, 'next@1.0.0', '2025-01-01T00:00:00Z');
194
+
195
+ // Simulate the post-release prerelease bump
196
+ await writePackage(root, next.name, next.path, next.version);
197
+ await git(root, ['add', '.']);
198
+ await git(root, ['commit', '-m', 'chore(release): prepare next prerelease']);
199
+
200
+ // A real fix lands on the prerelease version
201
+ await mkdir(join(root, next.path, 'src'), { recursive: true });
202
+ await writeFile(join(root, next.path, 'src/index.ts'), 'export const fix = true;\n');
203
+ await git(root, ['add', join(next.path, 'src/index.ts')]);
204
+ await git(root, ['commit', '-m', 'fix(next): repair something']);
205
+
206
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [next])).resolves.toEqual([next]);
207
+ });
208
+ });
209
+
210
+ it('does not select a prerelease package when no releasable changes exist since the last stable tag', async () => {
211
+ await withFixtureRepo(async (root) => {
212
+ await writePackage(root, next.name, next.path, '1.0.0');
213
+ await mkdir(join(root, next.path, 'src'), { recursive: true });
214
+ await writeFile(join(root, next.path, 'src/index.ts'), 'export const v1 = true;\n');
215
+ await git(root, ['add', '.']);
216
+ await git(root, ['commit', '-m', 'chore(release): publish']);
217
+ await tag(root, 'next@1.0.0', '2025-01-01T00:00:00Z');
218
+
219
+ // Only the prerelease bump, no releasable source changes
220
+ await writePackage(root, next.name, next.path, next.version);
221
+ await git(root, ['add', '.']);
222
+ await git(root, ['commit', '-m', 'chore(release): prepare next prerelease']);
223
+
224
+ await expect(autoReleaseCandidatePackages(gitCandidateShell(root), [next])).resolves.toEqual([]);
225
+ });
226
+ });
227
+
188
228
  it('uses a repaired project-name tag as the follow-up auto release baseline for a scoped package', async () => {
189
229
  await withFixtureRepo(async (root) => {
190
230
  await writePackage(root, cli.name, cli.path, cli.version);
@@ -220,6 +260,22 @@ function gitCandidateShell(
220
260
  const result = await $`git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
221
261
  return result.exitCode === 0;
222
262
  },
263
+ latestStableReleaseRef: async (projectName) => {
264
+ const pattern = `${projectName}@*`;
265
+ const result = await $`git tag --list ${pattern} --sort=-v:refname`.cwd(root).quiet().nothrow();
266
+ if (result.exitCode !== 0) {
267
+ return null;
268
+ }
269
+ const prefix = `${projectName}@`;
270
+ for (const line of new TextDecoder().decode(result.stdout).split('\n')) {
271
+ const tagName = line.trim();
272
+ const version = tagName.slice(prefix.length);
273
+ if (version && !version.includes('-')) {
274
+ return `refs/tags/${tagName}`;
275
+ }
276
+ }
277
+ return null;
278
+ },
223
279
  packageChangedFilesSince: async (ref, packagePath) => {
224
280
  const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
225
281
  if (result.exitCode === 0) {
@@ -4,6 +4,7 @@ import { releaseTag } from './core.js';
4
4
 
5
5
  export interface AutoReleaseCandidateShell {
6
6
  gitRefExists(ref: string): Promise<boolean>;
7
+ latestStableReleaseRef(projectName: string): Promise<string | null>;
7
8
  packageChangedFilesSince(ref: string, packagePath: string): Promise<string[]>;
8
9
  packageJsonAtRef(ref: string, packagePath: string): Promise<Record<string, unknown> | null>;
9
10
  currentPackageJson(packagePath: string): Promise<Record<string, unknown> | null>;
@@ -62,7 +63,19 @@ async function isAutoReleaseCandidate<Package extends ReleasePackageInfo>(
62
63
  if (await shell.gitRefExists(tagRef)) {
63
64
  return packageHasReleasableChangesSince(shell, tagRef, pkg);
64
65
  }
65
- return !pkg.version.includes('-') && shell.packageHasHistory(pkg.path);
66
+ if (pkg.version.includes('-')) {
67
+ // Prerelease version (e.g. 1.0.1-next.0) without its own tag: check for
68
+ // releasable changes since the last stable release. The prerelease bump is
69
+ // added after every release to make it obvious during development that the
70
+ // working version is unreleased — it should not suppress the next release.
71
+ const stableRef = await shell.latestStableReleaseRef(pkg.projectName);
72
+ if (stableRef) {
73
+ return packageHasReleasableChangesSince(shell, stableRef, pkg);
74
+ }
75
+ // No prior stable release — first release must be intentional, not auto.
76
+ return false;
77
+ }
78
+ return shell.packageHasHistory(pkg.path);
66
79
  }
67
80
 
68
81
  async function packageHasReleasableChangesSince<Package extends ReleasePackageInfo>(
@@ -531,6 +531,7 @@ async function releaseVersionPackages(
531
531
  return autoReleaseCandidatePackages(
532
532
  {
533
533
  gitRefExists: (ref) => gitRefExists(root, ref),
534
+ latestStableReleaseRef: (projectName) => latestStableReleaseRef(root, projectName),
534
535
  packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
535
536
  packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
536
537
  currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
@@ -1071,6 +1072,28 @@ async function gitRefExists(root: string, ref: string): Promise<boolean> {
1071
1072
  return result.exitCode === 0;
1072
1073
  }
1073
1074
 
1075
+ async function latestStableReleaseRef(root: string, projectName: string): Promise<string | null> {
1076
+ // List tags matching <projectName>@* sorted newest-first by version, then
1077
+ // return the first non-prerelease tag as a full ref.
1078
+ const pattern = `${projectName}@*`;
1079
+ const result = await $`git tag --list ${pattern} --sort=-v:refname`.cwd(root).quiet().nothrow();
1080
+ if (result.exitCode !== 0) {
1081
+ return null;
1082
+ }
1083
+ const tags = decode(result.stdout)
1084
+ .split('\n')
1085
+ .map((t) => t.trim())
1086
+ .filter(Boolean);
1087
+ const prefix = `${projectName}@`;
1088
+ for (const tagName of tags) {
1089
+ const version = tagName.slice(prefix.length);
1090
+ if (version && !version.includes('-')) {
1091
+ return `refs/tags/${tagName}`;
1092
+ }
1093
+ }
1094
+ return null;
1095
+ }
1096
+
1074
1097
  async function packageChangedFilesSince(root: string, ref: string, packagePath: string): Promise<string[]> {
1075
1098
  const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
1076
1099
  if (result.exitCode !== 0) {