@smoothbricks/cli 0.3.1 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,8BAA8B;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAWD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,8BAAmC,GAAG,MAAM,CA0C1G;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6BhE"}
1
+ {"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,8BAA8B;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAWD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,8BAAmC,GAAG,MAAM,CAkD1G;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiChE"}
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
@@ -20,6 +21,12 @@ export function syncBunLockfileVersions(root, options = {}) {
20
21
  let lockfile = readFileSync(lockfilePath, 'utf8');
21
22
  let updated = 0;
22
23
  for (const pkg of packages) {
24
+ // If the package.json version is a prerelease (e.g. 0.2.2-next.0) it may
25
+ // never have been published. Use the latest stable git tag version so
26
+ // that `bun pm pack` writes an installable dependency range for consumers.
27
+ const targetVersion = pkg.version.includes('-')
28
+ ? (latestStableTagVersion(root, pkg.projectName) ?? pkg.version)
29
+ : pkg.version;
23
30
  const relativePath = pkg.path.replaceAll('\\', '/');
24
31
  const escaped = escapeRegex(relativePath);
25
32
  const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
@@ -31,15 +38,16 @@ export function syncBunLockfileVersions(root, options = {}) {
31
38
  continue;
32
39
  }
33
40
  const lockVersion = match[2];
34
- if (lockVersion === pkg.version) {
41
+ if (lockVersion === targetVersion) {
35
42
  if (log) {
36
- console.log(`ok: ${relativePath} = ${pkg.version}`);
43
+ console.log(`ok: ${relativePath} = ${targetVersion}`);
37
44
  }
38
45
  continue;
39
46
  }
40
- lockfile = lockfile.replace(pattern, `$1${pkg.version}$3`);
47
+ lockfile = lockfile.replace(pattern, `$1${targetVersion}$3`);
41
48
  if (log) {
42
- console.log(`fix: ${relativePath}: ${lockVersion} -> ${pkg.version}`);
49
+ const suffix = targetVersion !== pkg.version ? ` (latest stable tag; package.json has ${pkg.version})` : '';
50
+ console.log(`fix: ${relativePath}: ${lockVersion} -> ${targetVersion}${suffix}`);
43
51
  }
44
52
  updated++;
45
53
  }
@@ -61,6 +69,9 @@ export function validateBunLockfileVersions(root) {
61
69
  const lockfile = readFileSync(lockfilePath, 'utf8');
62
70
  let failures = 0;
63
71
  for (const pkg of packages) {
72
+ const targetVersion = pkg.version.includes('-')
73
+ ? (latestStableTagVersion(root, pkg.projectName) ?? pkg.version)
74
+ : pkg.version;
64
75
  const relativePath = pkg.path.replaceAll('\\', '/');
65
76
  const escaped = escapeRegex(relativePath);
66
77
  const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
@@ -71,8 +82,8 @@ export function validateBunLockfileVersions(root) {
71
82
  continue;
72
83
  }
73
84
  const lockVersion = match[2];
74
- if (lockVersion !== pkg.version) {
75
- console.error(`${relativePath}: bun.lock workspace version must be ${pkg.version}, got ${lockVersion}`);
85
+ if (lockVersion !== targetVersion) {
86
+ console.error(`${relativePath}: bun.lock workspace version must be ${targetVersion}, got ${lockVersion}`);
76
87
  failures++;
77
88
  }
78
89
  }
@@ -81,3 +92,26 @@ export function validateBunLockfileVersions(root) {
81
92
  }
82
93
  return failures;
83
94
  }
95
+ function latestStableTagVersion(root, projectName) {
96
+ try {
97
+ const output = execSync(`git tag --list '${projectName}@*' --sort=-v:refname`, {
98
+ cwd: root,
99
+ encoding: 'utf8',
100
+ stdio: ['pipe', 'pipe', 'pipe'],
101
+ });
102
+ const prefix = `${projectName}@`;
103
+ for (const line of output.split('\n')) {
104
+ const tag = line.trim();
105
+ if (!tag.startsWith(prefix))
106
+ continue;
107
+ const version = tag.slice(prefix.length);
108
+ if (version && !version.includes('-')) {
109
+ return version;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
@@ -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 +1 @@
1
- {"version":3,"file":"packed-manifest.d.ts","sourceRoot":"","sources":["../../src/monorepo/packed-manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAUlC;AAED,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,WAAW,EAC1B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,EAAE,CAoCV"}
1
+ {"version":3,"file":"packed-manifest.d.ts","sourceRoot":"","sources":["../../src/monorepo/packed-manifest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAUlC;AAED,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,WAAW,EAC1B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,MAAM,EAAE,CA8CV"}
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { $ } from 'bun';
2
3
  import { isRecord, recordProperty } from '../lib/json.js';
3
4
  import { decode } from '../lib/run.js';
@@ -14,7 +15,9 @@ export async function readPackedPackageJson(root, tarball, packageName) {
14
15
  return parsed;
15
16
  }
16
17
  export function validatePackedWorkspaceDependencies(root, sourcePackage, packedPackage) {
17
- const workspaceVersions = new Map(getWorkspacePackages(root).map((pkg) => [pkg.name, pkg.version]));
18
+ const workspacePackages = getWorkspacePackages(root);
19
+ const workspaceVersions = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.version]));
20
+ const projectNameByPackage = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.projectName]));
18
21
  const failures = [];
19
22
  for (const field of workspaceDependencyFields) {
20
23
  const sourceDependencies = recordProperty(sourcePackage.json, field);
@@ -39,8 +42,15 @@ export function validatePackedWorkspaceDependencies(root, sourcePackage, packedP
39
42
  if (sourceRange !== 'workspace:*') {
40
43
  failures.push(`${sourcePackage.path}: source ${field}.${name} must use workspace:*`);
41
44
  }
42
- if (packedRange !== workspaceVersion) {
43
- failures.push(`${sourcePackage.path}: packed ${field}.${name} must be ${workspaceVersion}, got ${formatRange(packedRange)}`);
45
+ // When the workspace dep is at a prerelease version that was never
46
+ // published, the lockfile sync rewrites it to the latest stable tag
47
+ // version. Accept that version as valid.
48
+ const projectName = projectNameByPackage.get(name);
49
+ const expectedVersion = workspaceVersion.includes('-') && projectName
50
+ ? (latestStableTagVersion(root, projectName) ?? workspaceVersion)
51
+ : workspaceVersion;
52
+ if (packedRange !== expectedVersion) {
53
+ failures.push(`${sourcePackage.path}: packed ${field}.${name} must be ${expectedVersion}, got ${formatRange(packedRange)}`);
44
54
  }
45
55
  }
46
56
  }
@@ -49,3 +59,26 @@ export function validatePackedWorkspaceDependencies(root, sourcePackage, packedP
49
59
  function formatRange(value) {
50
60
  return typeof value === 'string' ? value : '<missing>';
51
61
  }
62
+ function latestStableTagVersion(root, projectName) {
63
+ try {
64
+ const output = execSync(`git tag --list '${projectName}@*' --sort=-v:refname`, {
65
+ cwd: root,
66
+ encoding: 'utf8',
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ });
69
+ const prefix = `${projectName}@`;
70
+ for (const line of output.split('\n')) {
71
+ const tag = line.trim();
72
+ if (!tag.startsWith(prefix))
73
+ continue;
74
+ const version = tag.slice(prefix.length);
75
+ if (version && !version.includes('-')) {
76
+ return version;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
@@ -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,13 +2,7 @@
2
2
  cd "$(git rev-parse --show-toplevel)"
3
3
  TOOLING="$PWD/tooling"
4
4
 
5
- # PATH order: most-specific → least-specific.
6
- # 1. git-hooks/ – hook-specific helper scripts
7
- # 2. tooling/ – the repo toolbox (curated tools like the smoo wrapper)
8
- # 3. tooling/node_modules/.bin – toolbox installed deps (@smoothbricks/cli)
9
- # 4. node_modules/.bin – root workspace deps (Nx, biome, etc.)
10
- # 5. devenv profile – upstream native/system tools
11
- export PATH="$TOOLING/git-hooks:$TOOLING:$TOOLING/node_modules/.bin:$PWD/node_modules/.bin:$TOOLING/direnv/.devenv/profile/bin:$PATH"
5
+ export PATH="$("$TOOLING/direnv/repo-path")"
12
6
 
13
7
  set -e -o pipefail
14
8
 
@@ -2,13 +2,7 @@
2
2
  cd "$(git rev-parse --show-toplevel)"
3
3
  TOOLING="$PWD/tooling"
4
4
 
5
- # PATH order: most-specific → least-specific.
6
- # 1. git-hooks/ – hook-specific helper scripts
7
- # 2. tooling/ – the repo toolbox (curated tools like the smoo wrapper)
8
- # 3. tooling/node_modules/.bin – toolbox installed deps (@smoothbricks/cli)
9
- # 4. node_modules/.bin – root workspace deps (Nx, biome, etc.)
10
- # 5. devenv profile – upstream native/system tools
11
- export PATH="$TOOLING/git-hooks:$TOOLING:$TOOLING/node_modules/.bin:$PWD/node_modules/.bin:$TOOLING/direnv/.devenv/profile/bin:$PATH"
5
+ export PATH="$("$TOOLING/direnv/repo-path")"
12
6
 
13
7
  set -e -o pipefail
14
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -45,7 +45,7 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@arethetypeswrong/cli": "^0.18.2",
48
- "@smoothbricks/nx-plugin": "0.2.1",
48
+ "@smoothbricks/nx-plugin": "0.2.2",
49
49
  "@smoothbricks/validation": "0.1.1",
50
50
  "commander": "^14.0.3",
51
51
  "publint": "^0.3.18",
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
@@ -25,6 +26,13 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
25
26
  let lockfile = readFileSync(lockfilePath, 'utf8');
26
27
  let updated = 0;
27
28
  for (const pkg of packages) {
29
+ // If the package.json version is a prerelease (e.g. 0.2.2-next.0) it may
30
+ // never have been published. Use the latest stable git tag version so
31
+ // that `bun pm pack` writes an installable dependency range for consumers.
32
+ const targetVersion = pkg.version.includes('-')
33
+ ? (latestStableTagVersion(root, pkg.projectName) ?? pkg.version)
34
+ : pkg.version;
35
+
28
36
  const relativePath = pkg.path.replaceAll('\\', '/');
29
37
  const escaped = escapeRegex(relativePath);
30
38
  const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
@@ -36,15 +44,16 @@ export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVe
36
44
  continue;
37
45
  }
38
46
  const lockVersion = match[2];
39
- if (lockVersion === pkg.version) {
47
+ if (lockVersion === targetVersion) {
40
48
  if (log) {
41
- console.log(`ok: ${relativePath} = ${pkg.version}`);
49
+ console.log(`ok: ${relativePath} = ${targetVersion}`);
42
50
  }
43
51
  continue;
44
52
  }
45
- lockfile = lockfile.replace(pattern, `$1${pkg.version}$3`);
53
+ lockfile = lockfile.replace(pattern, `$1${targetVersion}$3`);
46
54
  if (log) {
47
- console.log(`fix: ${relativePath}: ${lockVersion} -> ${pkg.version}`);
55
+ const suffix = targetVersion !== pkg.version ? ` (latest stable tag; package.json has ${pkg.version})` : '';
56
+ console.log(`fix: ${relativePath}: ${lockVersion} -> ${targetVersion}${suffix}`);
48
57
  }
49
58
  updated++;
50
59
  }
@@ -69,6 +78,10 @@ export function validateBunLockfileVersions(root: string): number {
69
78
  const lockfile = readFileSync(lockfilePath, 'utf8');
70
79
  let failures = 0;
71
80
  for (const pkg of packages) {
81
+ const targetVersion = pkg.version.includes('-')
82
+ ? (latestStableTagVersion(root, pkg.projectName) ?? pkg.version)
83
+ : pkg.version;
84
+
72
85
  const relativePath = pkg.path.replaceAll('\\', '/');
73
86
  const escaped = escapeRegex(relativePath);
74
87
  const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
@@ -79,8 +92,8 @@ export function validateBunLockfileVersions(root: string): number {
79
92
  continue;
80
93
  }
81
94
  const lockVersion = match[2];
82
- if (lockVersion !== pkg.version) {
83
- console.error(`${relativePath}: bun.lock workspace version must be ${pkg.version}, got ${lockVersion}`);
95
+ if (lockVersion !== targetVersion) {
96
+ console.error(`${relativePath}: bun.lock workspace version must be ${targetVersion}, got ${lockVersion}`);
84
97
  failures++;
85
98
  }
86
99
  }
@@ -89,3 +102,25 @@ export function validateBunLockfileVersions(root: string): number {
89
102
  }
90
103
  return failures;
91
104
  }
105
+
106
+ function latestStableTagVersion(root: string, projectName: string): string | null {
107
+ try {
108
+ const output = execSync(`git tag --list '${projectName}@*' --sort=-v:refname`, {
109
+ cwd: root,
110
+ encoding: 'utf8',
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ });
113
+ const prefix = `${projectName}@`;
114
+ for (const line of output.split('\n')) {
115
+ const tag = line.trim();
116
+ if (!tag.startsWith(prefix)) continue;
117
+ const version = tag.slice(prefix.length);
118
+ if (version && !version.includes('-')) {
119
+ return version;
120
+ }
121
+ }
122
+ return null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
@@ -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',
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { $ } from 'bun';
2
3
  import { isRecord, recordProperty } from '../lib/json.js';
3
4
  import { decode } from '../lib/run.js';
@@ -25,7 +26,9 @@ export function validatePackedWorkspaceDependencies(
25
26
  sourcePackage: PackageInfo,
26
27
  packedPackage: Record<string, unknown>,
27
28
  ): string[] {
28
- const workspaceVersions = new Map(getWorkspacePackages(root).map((pkg) => [pkg.name, pkg.version]));
29
+ const workspacePackages = getWorkspacePackages(root);
30
+ const workspaceVersions = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.version]));
31
+ const projectNameByPackage = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.projectName]));
29
32
  const failures: string[] = [];
30
33
  for (const field of workspaceDependencyFields) {
31
34
  const sourceDependencies = recordProperty(sourcePackage.json, field);
@@ -52,9 +55,17 @@ export function validatePackedWorkspaceDependencies(
52
55
  if (sourceRange !== 'workspace:*') {
53
56
  failures.push(`${sourcePackage.path}: source ${field}.${name} must use workspace:*`);
54
57
  }
55
- if (packedRange !== workspaceVersion) {
58
+ // When the workspace dep is at a prerelease version that was never
59
+ // published, the lockfile sync rewrites it to the latest stable tag
60
+ // version. Accept that version as valid.
61
+ const projectName = projectNameByPackage.get(name);
62
+ const expectedVersion =
63
+ workspaceVersion.includes('-') && projectName
64
+ ? (latestStableTagVersion(root, projectName) ?? workspaceVersion)
65
+ : workspaceVersion;
66
+ if (packedRange !== expectedVersion) {
56
67
  failures.push(
57
- `${sourcePackage.path}: packed ${field}.${name} must be ${workspaceVersion}, got ${formatRange(packedRange)}`,
68
+ `${sourcePackage.path}: packed ${field}.${name} must be ${expectedVersion}, got ${formatRange(packedRange)}`,
58
69
  );
59
70
  }
60
71
  }
@@ -65,3 +76,25 @@ export function validatePackedWorkspaceDependencies(
65
76
  function formatRange(value: unknown): string {
66
77
  return typeof value === 'string' ? value : '<missing>';
67
78
  }
79
+
80
+ function latestStableTagVersion(root: string, projectName: string): string | null {
81
+ try {
82
+ const output = execSync(`git tag --list '${projectName}@*' --sort=-v:refname`, {
83
+ cwd: root,
84
+ encoding: 'utf8',
85
+ stdio: ['pipe', 'pipe', 'pipe'],
86
+ });
87
+ const prefix = `${projectName}@`;
88
+ for (const line of output.split('\n')) {
89
+ const tag = line.trim();
90
+ if (!tag.startsWith(prefix)) continue;
91
+ const version = tag.slice(prefix.length);
92
+ if (version && !version.includes('-')) {
93
+ return version;
94
+ }
95
+ }
96
+ return null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }