@smoothbricks/cli 0.10.4 → 0.10.6

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 (56) hide show
  1. package/dist/cli.js +5 -5
  2. package/dist/github-ci/index.d.ts.map +1 -1
  3. package/dist/github-ci/index.js +5 -3
  4. package/dist/lib/json.d.ts +5 -1
  5. package/dist/lib/json.d.ts.map +1 -1
  6. package/dist/lib/json.js +18 -16
  7. package/dist/monorepo/ci-workflow.d.ts +2 -0
  8. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  9. package/dist/monorepo/ci-workflow.js +10 -4
  10. package/dist/monorepo/github-runs-on.d.ts +14 -0
  11. package/dist/monorepo/github-runs-on.d.ts.map +1 -0
  12. package/dist/monorepo/github-runs-on.js +43 -0
  13. package/dist/monorepo/index.d.ts +3 -3
  14. package/dist/monorepo/index.d.ts.map +1 -1
  15. package/dist/monorepo/index.js +20 -8
  16. package/dist/monorepo/lockfile.d.ts +2 -1
  17. package/dist/monorepo/lockfile.d.ts.map +1 -1
  18. package/dist/monorepo/lockfile.js +1 -1
  19. package/dist/monorepo/managed-files.d.ts +30 -3
  20. package/dist/monorepo/managed-files.d.ts.map +1 -1
  21. package/dist/monorepo/managed-files.js +83 -63
  22. package/dist/monorepo/packs/index.js +2 -2
  23. package/dist/monorepo/publish-workflow.d.ts +3 -0
  24. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  25. package/dist/monorepo/publish-workflow.js +17 -14
  26. package/dist/monorepo/tool-validation.d.ts +2 -2
  27. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  28. package/dist/monorepo/tool-validation.js +110 -16
  29. package/dist/release/index.d.ts.map +1 -1
  30. package/dist/release/index.js +6 -0
  31. package/managed/raw/envrc +5 -3
  32. package/managed/raw/patches/ttsc@0.19.3.patch +67 -0
  33. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +24 -7
  34. package/managed/raw/tooling/direnv/setup-environment.ts +23 -3
  35. package/managed/templates/github/actions/cache-nix-devenv/action.yml +13 -11
  36. package/managed/templates/github/actions/save-nix-devenv/action.yml +34 -3
  37. package/managed/templates/github/actions/setup-devenv/action.yml +44 -15
  38. package/managed/templates/github/workflows/managed-files.yml +64 -12
  39. package/package.json +2 -2
  40. package/src/cli.ts +6 -6
  41. package/src/github-ci/index.test.ts +3 -3
  42. package/src/github-ci/index.ts +6 -3
  43. package/src/lib/json.ts +5 -1
  44. package/src/monorepo/__tests__/ci-workflow.test.ts +37 -1
  45. package/src/monorepo/__tests__/publish-workflow.test.ts +28 -1
  46. package/src/monorepo/ci-workflow.ts +14 -4
  47. package/src/monorepo/github-runs-on.ts +45 -0
  48. package/src/monorepo/index.ts +20 -8
  49. package/src/monorepo/lockfile.ts +3 -2
  50. package/src/monorepo/managed-files.test.ts +49 -0
  51. package/src/monorepo/managed-files.ts +105 -64
  52. package/src/monorepo/packs/index.ts +2 -2
  53. package/src/monorepo/publish-workflow.ts +23 -15
  54. package/src/monorepo/tool-validation.test.ts +38 -16
  55. package/src/monorepo/tool-validation.ts +114 -16
  56. package/src/release/index.ts +6 -0
@@ -13,13 +13,20 @@ import {
13
13
  const registryVersions: Record<string, string[]> = {
14
14
  '@biomejs/biome': ['2.3.5'],
15
15
  '@nx/js': ['23.1.0'],
16
+ '@smoothbricks/cli': ['0.10.3', '0.10.4'],
16
17
  '@smoothbricks/nx-plugin': ['0.3.0'],
17
18
  eslint: ['9.39.1'],
18
19
  'eslint-stdout': ['1.1.1', '1.1.2'],
19
20
  nx: ['23.1.0'],
20
21
  prettier: ['3.6.1'],
21
- ttsc: ['0.18.4'],
22
- typescript: ['5.9.3'],
22
+ ttsc: ['0.19.3'],
23
+ typescript: ['6.0.3'],
24
+ // alias package is not fetched from registry under this name when using fallback
25
+ '@typescript/native': ['7.0.2'],
26
+ };
27
+
28
+ const registryLatestTags: Record<string, string> = {
29
+ '@smoothbricks/cli': '0.10.4',
23
30
  };
24
31
 
25
32
  const realFetch = globalThis.fetch;
@@ -58,9 +65,9 @@ describe('tool configuration validation', () => {
58
65
  `,
59
66
  );
60
67
 
61
- expect(validateToolConfig(root)).toBeGreaterThan(0);
68
+ expect(await validateToolConfig(root)).toBeGreaterThan(0);
62
69
  await applyToolConfigDefaults(root);
63
- expect(validateToolConfig(root)).toBe(0);
70
+ expect(await validateToolConfig(root)).toBe(0);
64
71
 
65
72
  const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
66
73
  const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
@@ -69,6 +76,10 @@ describe('tool configuration validation', () => {
69
76
  expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
70
77
  expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
71
78
  expect(rootPackage.devDependencies.nx).toBe('23.1.0');
79
+ expect(rootPackage.devDependencies.ttsc).toBe('0.19.3');
80
+ expect(rootPackage.devDependencies.typescript).toBe('^6.0.3');
81
+ expect(rootPackage.devDependencies['@typescript/native']).toBe('npm:typescript@^7.0.2');
82
+ expect(rootPackage.patchedDependencies['ttsc@0.19.3']).toBe('patches/ttsc@0.19.3.patch');
72
83
  expect(rootPackage.workspaces).toContain('tooling');
73
84
  expect(toolingPackage.name).toBe('@smoothbricks/tooling');
74
85
  expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
@@ -97,8 +108,12 @@ describe('tool configuration validation', () => {
97
108
  'eslint-stdout': await currentEslintStdoutRange(),
98
109
  nx: '24.0.0',
99
110
  prettier: '^3.7.0',
100
- ttsc: '^0.19.0',
111
+ ttsc: '0.19.3',
101
112
  typescript: '^6.0.0',
113
+ '@typescript/native': 'npm:typescript@^7.0.2',
114
+ },
115
+ patchedDependencies: {
116
+ 'ttsc@0.19.3': 'patches/ttsc@0.19.3.patch',
102
117
  },
103
118
  });
104
119
  await writeJson(join(root, 'tooling/package.json'), {
@@ -128,18 +143,17 @@ describe('tool configuration validation', () => {
128
143
  `,
129
144
  );
130
145
 
131
- expect(validateToolConfig(root)).toBe(0);
146
+ expect(await validateToolConfig(root)).toBe(0);
132
147
  } finally {
133
148
  await rm(root, { recursive: true, force: true });
134
149
  }
135
150
  });
136
151
 
137
- it('preserves an installable CLI range when the running CLI is a linked prerelease', async () => {
152
+ it('bumps consumer CLI pin to npm latest even when the running CLI is a linked prerelease', async () => {
138
153
  const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
139
154
  try {
140
155
  const configuredRange = '^0.10.1';
141
- const runningRange = await currentCliRange();
142
- const expectedRange = runningRange.includes('-') ? configuredRange : runningRange;
156
+ const expectedRange = `^${registryLatestTags['@smoothbricks/cli']}`;
143
157
  await writeJson(join(root, 'package.json'), {
144
158
  name: '@fixture/app',
145
159
  version: '0.0.0',
@@ -152,7 +166,7 @@ describe('tool configuration validation', () => {
152
166
  dependencies: { '@smoothbricks/cli': configuredRange },
153
167
  });
154
168
 
155
- const context = readToolContext(root);
169
+ const context = await readToolContext(root);
156
170
  expect(context.policy.cliDependencyRange).toBe(expectedRange);
157
171
  applyToolingPackageDefaults(root, context.policy);
158
172
 
@@ -172,7 +186,7 @@ describe('tool configuration validation', () => {
172
186
  private: true,
173
187
  workspaces: ['packages/*', 'tooling'],
174
188
  });
175
- applyToolingPackageDefaults(root, readToolContext(root).policy);
189
+ applyToolingPackageDefaults(root, (await readToolContext(root)).policy);
176
190
 
177
191
  const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
178
192
  expect(toolingPackage.name).toBe('@smoothbricks/tooling');
@@ -196,12 +210,12 @@ describe('tool configuration validation', () => {
196
210
  eslint: '^10.0.0',
197
211
  nx: '24.0.0',
198
212
  prettier: '^3.7.0',
199
- ttsc: '^0.19.0',
213
+ ttsc: '0.19.3',
200
214
  typescript: '^6.0.0',
201
215
  },
202
216
  });
203
217
 
204
- const context = readToolContext(root);
218
+ const context = await readToolContext(root);
205
219
  await applyRootDevDependencyDefaults(root, context);
206
220
  applyToolingPackageDefaults(root, context.policy);
207
221
 
@@ -209,6 +223,8 @@ describe('tool configuration validation', () => {
209
223
  const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
210
224
  expect(rootPackage.devDependencies['eslint-stdout']).toBe(await currentEslintStdoutRange());
211
225
  expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe(await currentNxPluginRange());
226
+ expect(rootPackage.devDependencies['@typescript/native']).toBe('npm:typescript@^7.0.2');
227
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBe('patches/ttsc@0.19.3.patch');
212
228
  expect(toolingPackage.name).toBe('@fixture/tooling');
213
229
  expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(await currentCliRange());
214
230
  } finally {
@@ -218,8 +234,7 @@ describe('tool configuration validation', () => {
218
234
  });
219
235
 
220
236
  async function currentCliRange(): Promise<string> {
221
- const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
222
- return `^${pkg.version}`;
237
+ return `^${registryLatestTags['@smoothbricks/cli']}`;
223
238
  }
224
239
 
225
240
  async function currentEslintStdoutRange(): Promise<string> {
@@ -235,8 +250,15 @@ const mockRegistryFetch = Object.assign(
235
250
  if (!versions) {
236
251
  return Promise.resolve(new Response('{}', { status: 404 }));
237
252
  }
253
+ const body: Record<string, unknown> = {
254
+ versions: Object.fromEntries(versions.map((version) => [version, {}])),
255
+ };
256
+ const latest = registryLatestTags[packageName];
257
+ if (latest) {
258
+ body['dist-tags'] = { latest };
259
+ }
238
260
  return Promise.resolve(
239
- new Response(JSON.stringify({ versions: Object.fromEntries(versions.map((version) => [version, {}])) }), {
261
+ new Response(JSON.stringify(body), {
240
262
  status: 200,
241
263
  headers: { 'content-type': 'application/json' },
242
264
  }),
@@ -32,6 +32,7 @@ export interface ToolContext {
32
32
 
33
33
  interface RegistryPackument {
34
34
  versions: Record<string, unknown>;
35
+ 'dist-tags'?: Record<string, string>;
35
36
  }
36
37
 
37
38
  const isRegistryPackument = typia.createIs<RegistryPackument>();
@@ -55,10 +56,14 @@ const rootDevDependencies: RequiredDependency[] = [
55
56
  },
56
57
  { name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
57
58
  { name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', prefix: '^' },
58
- { name: 'ttsc', fallbackVersion: '^0.18.4', minimumVersion: '0.18.4', prefix: '^' },
59
+ // Exact pin: Bun patchedDependencies keys are version-locked (see patches/ttsc@0.19.3.patch).
60
+ { name: 'ttsc', fallbackVersion: '0.19.3' },
59
61
  // Nx and typescript-eslint still load the TypeScript JS API (6.x).
60
62
  // Compilation is exclusively delegated to ttsc by the Nx plugin targets.
61
- { name: 'typescript', fallbackVersion: '^5.9.3', minimumVersion: '5.9.0', prefix: '^' },
63
+ { name: 'typescript', fallbackVersion: '^6.0.3', minimumVersion: '6.0.0', prefix: '^' },
64
+ // TS7 native compiler for ttsc (TTSC_TSGO_BINARY). Not the unscoped API package.
65
+ // npm alias form required — Bun cannot install @typescript/typescript6 cleanly (bun#33834).
66
+ { name: '@typescript/native', fallbackVersion: 'npm:typescript@^7.0.2' },
62
67
  ];
63
68
 
64
69
  const cliPackageName = '@smoothbricks/cli';
@@ -70,17 +75,21 @@ const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejan
70
75
  // never against a version template here.
71
76
  const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
72
77
 
78
+ const TTSC_PATCHED_DEPENDENCY_KEY = 'ttsc@0.19.3';
79
+ const TTSC_PATCH_PATH = 'patches/ttsc@0.19.3.patch';
80
+
73
81
  export async function applyToolConfigDefaults(root: string): Promise<void> {
74
- const context = readToolContext(root);
82
+ const context = await readToolContext(root);
75
83
  await applyRootPackageToolDefaults(root, context);
76
84
  applyToolingPackageDefaults(root, context.policy);
77
85
  applyDevenvPackageDefaults(root);
78
86
  }
79
87
 
80
- export function validateToolConfig(root: string): number {
81
- const context = readToolContext(root);
88
+ export async function validateToolConfig(root: string): Promise<number> {
89
+ const context = await readToolContext(root);
82
90
  return (
83
91
  validateRootDevDependencies(context.policy, context.rootPackage) +
92
+ validateTtscPatchedDependency(context.rootPackage) +
84
93
  validateToolingPackage(root, context.policy) +
85
94
  validateToolingWorkspace(context.rootPackage) +
86
95
  validateDevenvPackages(root)
@@ -104,6 +113,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
104
113
  if (delete devDependencies['@smoothbricks/cli']) {
105
114
  changed = true;
106
115
  }
116
+ changed = ensureTtscPatchedDependency(pkg) || changed;
107
117
  if (changed) {
108
118
  writeJsonObject(join(root, 'package.json'), pkg);
109
119
  console.log('updated package.json workspace tool dependencies');
@@ -119,6 +129,7 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
119
129
  }
120
130
  let dependencyChanged = false;
121
131
  let workspaceChanged = false;
132
+ let patchChanged = false;
122
133
  const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
123
134
  for (const dependency of rootDevDependencies) {
124
135
  const current = devDependencies[dependency.name];
@@ -131,7 +142,8 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
131
142
  dependencyChanged = true;
132
143
  }
133
144
  workspaceChanged = addWorkspacePattern(pkg, 'tooling');
134
- if (dependencyChanged || workspaceChanged) {
145
+ patchChanged = ensureTtscPatchedDependency(pkg);
146
+ if (dependencyChanged || workspaceChanged || patchChanged) {
135
147
  writeJsonObject(join(root, 'package.json'), pkg);
136
148
  }
137
149
  console.log(
@@ -144,6 +156,11 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
144
156
  ? 'updated package.json tooling workspace'
145
157
  : 'unchanged package.json tooling workspace',
146
158
  );
159
+ console.log(
160
+ patchChanged
161
+ ? 'updated package.json patchedDependencies for ttsc'
162
+ : 'unchanged package.json patchedDependencies for ttsc',
163
+ );
147
164
  }
148
165
 
149
166
  export function applyToolingPackageDefaults(root: string, policy: ToolPolicy): void {
@@ -235,6 +252,26 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
235
252
  return failures;
236
253
  }
237
254
 
255
+ function ensureTtscPatchedDependency(pkg: PackageJson): boolean {
256
+ const patched = ensureDependencyMap(pkg, 'patchedDependencies');
257
+ return setStringProperty(patched, TTSC_PATCHED_DEPENDENCY_KEY, TTSC_PATCH_PATH);
258
+ }
259
+
260
+ function validateTtscPatchedDependency(rootPackage: PackageJson | null): number {
261
+ if (!rootPackage) {
262
+ return 0;
263
+ }
264
+ const actual = rootPackage.patchedDependencies?.[TTSC_PATCHED_DEPENDENCY_KEY];
265
+ if (actual === TTSC_PATCH_PATH) {
266
+ return 0;
267
+ }
268
+ console.error(
269
+ `package.json patchedDependencies.${TTSC_PATCHED_DEPENDENCY_KEY} must be ${TTSC_PATCH_PATH}` +
270
+ (typeof actual === 'string' ? `; found ${actual}` : ' (required for declaration-emit fix)'),
271
+ );
272
+ return 1;
273
+ }
274
+
238
275
  export function validateToolingPackage(root: string, policy: ToolPolicy): number {
239
276
  const path = join(root, 'tooling', 'package.json');
240
277
  const pkg = readJsonObject(path);
@@ -331,6 +368,10 @@ function satisfiesDependencyPolicy(policy: ToolPolicy, version: string, dependen
331
368
  if (workspaceDependencyExpected(policy, dependency)) {
332
369
  return version === 'workspace:*';
333
370
  }
371
+ // Dual-package native compiler: must stay an npm:typescript@7 alias, never unscoped "typescript".
372
+ if (dependency.name === '@typescript/native') {
373
+ return isTypeScriptNativeAlias(version);
374
+ }
334
375
  if (dependency.minimumVersion === undefined) {
335
376
  return version === dependency.fallbackVersion;
336
377
  }
@@ -342,37 +383,41 @@ function satisfiesDependencyPolicy(policy: ToolPolicy, version: string, dependen
342
383
  return compareVersions(parsed, minimum) >= 0;
343
384
  }
344
385
 
386
+ function isTypeScriptNativeAlias(version: string): boolean {
387
+ // Accept npm:typescript@7, npm:typescript@^7.0.2, npm:typescript@~7.0.2, etc.
388
+ return /^npm:typescript@(?:\^|~)?7(?:\.|$)/.test(version.trim());
389
+ }
390
+
345
391
  function formatExpectedDependency(policy: ToolPolicy, dependency: RequiredDependency): string {
346
392
  if (workspaceDependencyExpected(policy, dependency)) {
347
393
  return 'workspace:*';
348
394
  }
395
+ if (dependency.name === '@typescript/native') {
396
+ return 'npm:typescript@^7 (TypeScript 7 native compiler alias for ttsc)';
397
+ }
349
398
  return dependency.minimumVersion ? `>= ${dependency.minimumVersion}` : dependency.fallbackVersion;
350
399
  }
351
400
 
352
- export function readToolContext(root: string): ToolContext {
401
+ export async function readToolContext(root: string): Promise<ToolContext> {
353
402
  const rootPackage = readPackageJsonObject(join(root, 'package.json'));
354
403
  const toolingPackage = readJsonObject(join(root, 'tooling', 'package.json'));
355
404
  const configuredCliRange = toolingPackage?.dependencies?.[cliPackageName];
356
405
  return {
357
406
  rootPackage,
358
- policy: toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
407
+ policy: await toolPolicy(rootPackage, typeof configuredCliRange === 'string' ? configuredCliRange : null),
359
408
  };
360
409
  }
361
410
 
362
- function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): ToolPolicy {
411
+ async function toolPolicy(rootPackage: PackageJson | null, configuredCliRange: string | null): Promise<ToolPolicy> {
363
412
  const name = rootPackage?.name ?? null;
364
413
  const isCodebase = isSmoothBricksCodebasePackageName(name ?? undefined);
365
414
  const toolingName = toolingPackageName(name);
366
- // A locally linked prerelease can be newer than every published CLI. Keep an existing
367
- // installable range instead of rewriting the consumer manifest to an unpublished version.
368
- const publishedCliRange =
369
- cliPackageVersion.includes('-') && configuredCliRange && parseVersion(configuredCliRange)
370
- ? configuredCliRange
371
- : `^${cliPackageVersion}`;
372
415
  return {
373
416
  isSmoothBricksCodebase: isCodebase,
374
417
  toolingPackageName: toolingName,
375
- cliDependencyRange: isCodebase ? 'workspace:*' : publishedCliRange,
418
+ // Consumers pin the latest *published* CLI. Running a linked prerelease must not
419
+ // freeze tooling/package.json on an older range or rewrite it to an unpublished -next.
420
+ cliDependencyRange: isCodebase ? 'workspace:*' : await resolvePublishedCliDependencyRange(configuredCliRange),
376
421
  };
377
422
  }
378
423
 
@@ -397,6 +442,59 @@ async function resolveDependencyVersion(policy: ToolPolicy, dependency: Required
397
442
  return `${dependency.prefix ?? ''}${latest ?? stripRangePrefix(dependency.fallbackVersion)}`;
398
443
  }
399
444
 
445
+ /**
446
+ * Consumer monorepos pin `@smoothbricks/cli` to the latest stable release.
447
+ * Prefer npm `dist-tags.latest`. Never write a running `-next` package version into
448
+ * consumer manifests; never leave an older pin just because the running CLI is prerelease.
449
+ */
450
+ async function resolvePublishedCliDependencyRange(configuredCliRange: string | null): Promise<string> {
451
+ const latest = await fetchLatestStableVersion(cliPackageName);
452
+ if (latest) {
453
+ return `^${latest}`;
454
+ }
455
+ // Registry unavailable: if the running CLI is a stable publish, use it.
456
+ if (!cliPackageVersion.includes('-')) {
457
+ return `^${cliPackageVersion}`;
458
+ }
459
+ // Linked prerelease + offline down: keep an existing installable stable pin.
460
+ if (configuredCliRange && parseVersion(configuredCliRange)) {
461
+ return configuredCliRange;
462
+ }
463
+ // Last resort: caret of the prerelease base (0.10.5-next.0 → ^0.10.5) so manifests stay valid.
464
+ const base = parseVersion(cliPackageVersion);
465
+ return base ? `^${base.major}.${base.minor}.${base.patch}` : `^${cliPackageVersion}`;
466
+ }
467
+
468
+ async function fetchLatestStableVersion(packageName: string): Promise<string | null> {
469
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`;
470
+ const response = await fetch(url, { headers: { accept: 'application/vnd.npm.install-v1+json' } });
471
+ if (!response.ok) {
472
+ return null;
473
+ }
474
+ const body: unknown = await response.json();
475
+ if (!isRegistryPackument(body)) {
476
+ return null;
477
+ }
478
+ const tagged = body['dist-tags']?.latest;
479
+ if (typeof tagged === 'string' && parseVersion(tagged) && !tagged.includes('-')) {
480
+ return tagged;
481
+ }
482
+ let latest: Version | null = null;
483
+ for (const raw of Object.keys(body.versions)) {
484
+ if (raw.includes('-')) {
485
+ continue;
486
+ }
487
+ const version = parseVersion(raw);
488
+ if (!version) {
489
+ continue;
490
+ }
491
+ if (!latest || compareVersions(version, latest) > 0) {
492
+ latest = version;
493
+ }
494
+ }
495
+ return latest ? `${latest.major}.${latest.minor}.${latest.patch}` : null;
496
+ }
497
+
400
498
  async function fetchLatestPatchVersion(packageName: string, minimumVersion: string): Promise<string | null> {
401
499
  const minorRange = sameMajorMinorRange(minimumVersion);
402
500
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`;
@@ -20,6 +20,7 @@ import {
20
20
  } from '../lib/json.js';
21
21
  import { decode, run, runInteractiveStatus, runResult } from '../lib/run.js';
22
22
  import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
23
+ import { syncBunLockfileVersions } from '../monorepo/lockfile.js';
23
24
  import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
24
25
  import {
25
26
  type BootstrapNpmPackagesOptions,
@@ -529,6 +530,10 @@ async function listUnpublishedPackages(root: string, packages: ReleasePackage[])
529
530
  async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: string, dryRun: boolean): Promise<void> {
530
531
  const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
531
532
  const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
533
+ // bun pm pack resolves workspace:* from bun.lock. Day-to-day lock keeps -next
534
+ // (install/CI). For publish, temporarily rewrite unpublished -next entries to
535
+ // the last stable tag so the tarball embeds installable versions, then restore.
536
+ syncBunLockfileVersions(root, { mode: 'publish', log: true });
532
537
  try {
533
538
  console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
534
539
  await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
@@ -560,6 +565,7 @@ async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: stri
560
565
  },
561
566
  );
562
567
  } finally {
568
+ syncBunLockfileVersions(root, { mode: 'install', log: true });
563
569
  await rm(tempDir, { recursive: true, force: true });
564
570
  }
565
571
  }