@smoothbricks/cli 0.10.6 → 0.10.8

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 (82) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +24 -1
  3. package/dist/github-ci/index.d.ts +44 -3
  4. package/dist/github-ci/index.d.ts.map +1 -1
  5. package/dist/github-ci/index.js +242 -36
  6. package/dist/lib/json.d.ts +1 -1
  7. package/dist/lib/json.d.ts.map +1 -1
  8. package/dist/lib/json.js +3 -18
  9. package/dist/lib/workspace.d.ts.map +1 -1
  10. package/dist/lib/workspace.js +1 -0
  11. package/dist/monorepo/ci-workflow.js +16 -6
  12. package/dist/monorepo/managed-files.d.ts +6 -22
  13. package/dist/monorepo/managed-files.d.ts.map +1 -1
  14. package/dist/monorepo/managed-files.js +31 -48
  15. package/dist/monorepo/package-policy.js +2 -2
  16. package/dist/monorepo/packs/index.d.ts.map +1 -1
  17. package/dist/monorepo/packs/index.js +2 -0
  18. package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
  19. package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
  20. package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
  21. package/dist/monorepo/publish-workflow.js +3 -3
  22. package/dist/monorepo/runtime.d.ts +2 -2
  23. package/dist/monorepo/runtime.d.ts.map +1 -1
  24. package/dist/monorepo/runtime.js +11 -16
  25. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  26. package/dist/monorepo/tool-validation.js +105 -28
  27. package/dist/nx/index.d.ts +7 -4
  28. package/dist/nx/index.d.ts.map +1 -1
  29. package/dist/nx/index.js +117 -56
  30. package/dist/playwright/index.d.ts +22 -0
  31. package/dist/playwright/index.d.ts.map +1 -0
  32. package/dist/playwright/index.js +44 -0
  33. package/dist/release/bootstrap-npm-packages.d.ts +3 -0
  34. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
  35. package/dist/release/bootstrap-npm-packages.js +21 -0
  36. package/dist/release/index.d.ts.map +1 -1
  37. package/dist/release/index.js +17 -0
  38. package/dist/wrangler/cloudflare.d.ts +85 -0
  39. package/dist/wrangler/cloudflare.d.ts.map +1 -0
  40. package/dist/wrangler/cloudflare.js +235 -0
  41. package/dist/wrangler/deploy-environment.d.ts +48 -0
  42. package/dist/wrangler/deploy-environment.d.ts.map +1 -0
  43. package/dist/wrangler/deploy-environment.js +383 -0
  44. package/dist/wrangler/environment.d.ts +58 -0
  45. package/dist/wrangler/environment.d.ts.map +1 -0
  46. package/dist/wrangler/environment.js +297 -0
  47. package/managed/raw/tooling/devenv +71 -0
  48. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
  49. package/package.json +11 -4
  50. package/src/cli.ts +33 -3
  51. package/src/github-ci/index.test.ts +221 -1
  52. package/src/github-ci/index.ts +303 -32
  53. package/src/lib/json.ts +1 -1
  54. package/src/lib/workspace.ts +1 -0
  55. package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
  56. package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
  57. package/src/monorepo/__tests__/publish-workflow.test.ts +12 -4
  58. package/src/monorepo/ci-workflow.ts +16 -6
  59. package/src/monorepo/managed-files.test.ts +87 -23
  60. package/src/monorepo/managed-files.ts +35 -69
  61. package/src/monorepo/package-policy.test.ts +31 -0
  62. package/src/monorepo/package-policy.ts +2 -2
  63. package/src/monorepo/packs/index.ts +2 -0
  64. package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
  65. package/src/monorepo/publish-workflow.ts +7 -2
  66. package/src/monorepo/runtime.test.ts +23 -17
  67. package/src/monorepo/runtime.ts +12 -17
  68. package/src/monorepo/tool-validation.test.ts +100 -8
  69. package/src/monorepo/tool-validation.ts +115 -31
  70. package/src/nx/index.test.ts +45 -14
  71. package/src/nx/index.ts +133 -64
  72. package/src/playwright/index.test.ts +90 -0
  73. package/src/playwright/index.ts +73 -0
  74. package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
  75. package/src/release/bootstrap-npm-packages.ts +34 -0
  76. package/src/release/index.ts +18 -0
  77. package/src/wrangler/cloudflare.ts +289 -0
  78. package/src/wrangler/deploy-environment.test.ts +354 -0
  79. package/src/wrangler/deploy-environment.ts +445 -0
  80. package/src/wrangler/environment.test.ts +173 -0
  81. package/src/wrangler/environment.ts +366 -0
  82. package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
@@ -486,7 +486,7 @@ function deployProductionStep(step: PublishWorkflowStep, options: PublishWorkflo
486
486
  " ${{ steps.version.outputs.mode != 'none' && inputs.deploy_environment == 'production' && inputs.dry_run !=",
487
487
  " 'true' }}",
488
488
  ...deployEnvLines(options),
489
- ' run: smoo github-ci nx-deploy --configuration production --mode run-many --verify --name "Deploy Production"',
489
+ ' run: smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
490
490
  ];
491
491
  }
492
492
 
@@ -785,6 +785,11 @@ function renderMacosPlatformSteps(options: PublishWorkflowDefinitionOptions): st
785
785
  ` "${githubExpression('runner.temp')}/macos-platform-outputs"`,
786
786
  '',
787
787
  ` # Step ${stepNumber++}`,
788
+ ' - name: 🧪 Unit test macOS and iOS packages',
789
+ ' run:',
790
+ ` smoo github-ci nx-run-many --targets test --projects-with-targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}"`,
791
+ '',
792
+ ` # Step ${stepNumber++}`,
788
793
  ' - name: 📤 Upload macOS platform outputs',
789
794
  ' uses: actions/upload-artifact@v7.0.1',
790
795
  ' with:',
@@ -935,7 +940,7 @@ function renderFinalLinuxPublishSteps(options: PublishWorkflowDefinitionOptions)
935
940
  " ${{ needs.linux-release-candidate.outputs.mode != 'none' && inputs.deploy_environment == 'production' &&",
936
941
  " inputs.dry_run != 'true' }}",
937
942
  ...deployEnvLines(options),
938
- ' run: smoo github-ci nx-deploy --configuration production --mode run-many --verify --name "Deploy Production"',
943
+ ' run: smoo github-ci nx-deploy --environment production --mode run-many --verify --name "Deploy Production"',
939
944
  );
940
945
  }
941
946
  lines.push(
@@ -3,16 +3,16 @@ import type { PackageJson } from '../lib/json.js';
3
3
  import { runtimeTypesRangeForPublishedVersions, validateRuntimePins } from './runtime.js';
4
4
 
5
5
  describe('runtimeTypesRangeForPublishedVersions', () => {
6
- test('uses the newest published minor within the installed Node major, never the ~major.0.0 floor', () => {
6
+ test('anchors a caret range at the newest published version in the installed Node major', () => {
7
7
  expect(
8
8
  runtimeTypesRangeForPublishedVersions('@types/node', '24.12.0', 'major', ['24.0.0', '24.12.4', '25.9.1']),
9
- ).toBe('~24.12.4');
9
+ ).toBe('^24.12.4');
10
10
  });
11
11
 
12
- test('falls back to latest published @types/node when the Node major is unpublished', () => {
13
- expect(
12
+ test('rejects fallback types from a different Node major', () => {
13
+ expect(() =>
14
14
  runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['24.12.4', '25.9.0', '25.9.1']),
15
- ).toBe('~25.9.1');
15
+ ).toThrow('runtime major 26');
16
16
  });
17
17
 
18
18
  test('uses the installed Bun version when @types/bun has published it', () => {
@@ -23,10 +23,10 @@ describe('runtimeTypesRangeForPublishedVersions', () => {
23
23
  expect(runtimeTypesRangeForPublishedVersions('@types/bun', '1.3.15', 'exact', ['1.3.13', '1.3.14'])).toBe('1.3.14');
24
24
  });
25
25
 
26
- test('ignores non-stable version strings when choosing the fallback', () => {
27
- expect(runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['25.9.1', '26.0.0-beta.1'])).toBe(
28
- '~25.9.1',
29
- );
26
+ test('ignores non-stable version strings when choosing the same-major anchor', () => {
27
+ expect(
28
+ runtimeTypesRangeForPublishedVersions('@types/node', '26.0.0', 'major', ['25.9.1', '26.0.2', '26.1.0-beta.1']),
29
+ ).toBe('^26.0.2');
30
30
  });
31
31
  });
32
32
 
@@ -35,23 +35,29 @@ describe('validateRuntimePins', () => {
35
35
  const aligned = () => ({
36
36
  engines: { node: '>=24.0.0' },
37
37
  packageManager: 'bun@1.3.14',
38
- devDependencies: { '@types/node': '~24.13.0' },
38
+ devDependencies: { '@types/node': '^24.13.0' },
39
39
  });
40
40
 
41
- test('passes when every pin agrees with the PATH runtimes', () => {
42
- expect(validateRuntimePins(aligned(), runtime)).toBe(0);
41
+ test('accepts caret ranges anchored at any patch or minor in the PATH Node major', () => {
42
+ for (const version of ['^24.0.3', '^24.13.0']) {
43
+ const pkg = aligned();
44
+ pkg.devDependencies['@types/node'] = version;
45
+ expect(validateRuntimePins(pkg, runtime)).toBe(0);
46
+ }
43
47
  });
44
48
 
45
49
  test('fails when @types/node tracks a different major than the PATH node', () => {
46
50
  const pkg = aligned();
47
- pkg.devDependencies['@types/node'] = '~26.1.1';
51
+ pkg.devDependencies['@types/node'] = '^26.1.1';
48
52
  expect(validateRuntimePins(pkg, runtime)).toBe(1);
49
53
  });
50
54
 
51
- test('fails the ~major.0.0 floor pin even when the major matches', () => {
52
- const pkg = aligned();
53
- pkg.devDependencies['@types/node'] = '~24.0.0';
54
- expect(validateRuntimePins(pkg, runtime)).toBe(1);
55
+ test('rejects exact and tilde @types/node pins even when the major matches', () => {
56
+ for (const version of ['24.13.0', '~24.13.0']) {
57
+ const pkg = aligned();
58
+ pkg.devDependencies['@types/node'] = version;
59
+ expect(validateRuntimePins(pkg, runtime)).toBe(1);
60
+ }
55
61
  });
56
62
 
57
63
  test('fails engines.node and packageManager drift against the PATH runtimes', () => {
@@ -66,8 +66,8 @@ export async function syncRootRuntimeVersions(root: string): Promise<void> {
66
66
  /**
67
67
  * Validate that package.json runtime pins agree with the LIVE PATH runtimes,
68
68
  * never a stored template. Offline by design: structural alignment only
69
- * (majors, exact bun pin, no `~major.0.0` floor) — the registry is not
70
- * consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
69
+ * (Node types float within the runtime major; Bun types stay exact) — the registry
70
+ * is not consulted. Repair: `smoo monorepo init --runtime-only` inside the devenv shell.
71
71
  */
72
72
  export async function validateRootRuntimeVersions(root: string): Promise<number> {
73
73
  const packageJson = readJsonObject(join(root, 'package.json'));
@@ -100,15 +100,10 @@ export function validateRuntimePins(packageJson: PackageJson, runtime: RuntimeVe
100
100
  failures++;
101
101
  }
102
102
  const typesNode = packageJson.devDependencies?.['@types/node'] ?? null;
103
- const typesNodeParts = typesNode ? /^~(\d+)\.(\d+)\.(\d+)$/.exec(typesNode) : null;
103
+ const typesNodeParts = typesNode ? /^\^(\d+)\.(\d+)\.(\d+)$/.exec(typesNode) : null;
104
104
  if (!typesNodeParts || typesNodeParts[1] !== nodeMajor) {
105
105
  console.error(
106
- `package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} — types track the runtime major (~${nodeMajor}.x.y, newest published minor); ${repair}`,
107
- );
108
- failures++;
109
- } else if (typesNodeParts[2] === '0' && typesNodeParts[3] === '0') {
110
- console.error(
111
- `package.json @types/node is pinned to the ~${nodeMajor}.0.0 floor — tilde locks the first patch line and strands the repo on early broken releases; ${repair} to repin to the newest published ${nodeMajor}.x`,
106
+ `package.json @types/node is ${typesNode ?? 'missing'} but the PATH node is v${runtime.node} — expected a caret range within the runtime major (^${nodeMajor}.x.y); ${repair}`,
112
107
  );
113
108
  failures++;
114
109
  }
@@ -144,15 +139,15 @@ export function runtimeTypesRangeForPublishedVersions(
144
139
 
145
140
  if (pinMode === 'major') {
146
141
  const runtimeMajor = parsedRuntimeVersion[0].toString();
147
- // Newest published types WITHIN the runtime major never the `~major.0.0`
148
- // floor: tilde locks the patch line, so the floor can strand consumers on a
149
- // broken early release (e.g. @types/node 24.0.x's URLPattern/DOM TS2403
150
- // conflict, fixed in 24.13) with no path to the repaired minors.
142
+ // Anchor the range at the newest published types in the runtime major. The caret
143
+ // admits later patches and minors without crossing into a different Node major.
151
144
  const latestInMajor = latestVersion(versions.filter((version) => versionMajor(version) === runtimeMajor));
152
- if (latestInMajor) {
153
- return `~${latestInMajor}`;
145
+ if (!latestInMajor) {
146
+ throw new Error(`Unable to find published ${packageName} versions for runtime major ${runtimeMajor}`);
154
147
  }
155
- } else if (versions.includes(runtimeVersion)) {
148
+ return `^${latestInMajor}`;
149
+ }
150
+ if (versions.includes(runtimeVersion)) {
156
151
  return runtimeVersion;
157
152
  }
158
153
 
@@ -162,7 +157,7 @@ export function runtimeTypesRangeForPublishedVersions(
162
157
  }
163
158
 
164
159
  console.warn(`${packageName} has no published types for runtime ${runtimeVersion}; using ${latest}`);
165
- return pinMode === 'major' ? `~${latest}` : latest;
160
+ return latest;
166
161
  }
167
162
 
168
163
  function latestVersion(versions: readonly string[]): string | null {
@@ -3,10 +3,12 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import {
6
+ applyDevenvPackageDefaults,
6
7
  applyRootDevDependencyDefaults,
7
8
  applyToolConfigDefaults,
8
9
  applyToolingPackageDefaults,
9
10
  readToolContext,
11
+ validateDevenvPackages,
10
12
  validateToolConfig,
11
13
  } from './tool-validation.js';
12
14
 
@@ -19,7 +21,7 @@ const registryVersions: Record<string, string[]> = {
19
21
  'eslint-stdout': ['1.1.1', '1.1.2'],
20
22
  nx: ['23.1.0'],
21
23
  prettier: ['3.6.1'],
22
- ttsc: ['0.19.3'],
24
+ ttsc: ['0.25.0'],
23
25
  typescript: ['6.0.3'],
24
26
  // alias package is not fetched from registry under this name when using fallback
25
27
  '@typescript/native': ['7.0.2'],
@@ -50,6 +52,9 @@ describe('tool configuration validation', () => {
50
52
  devDependencies: {
51
53
  '@smoothbricks/cli': 'workspace:*',
52
54
  },
55
+ patchedDependencies: {
56
+ 'ttsc@0.19.3': 'patches/ttsc@0.19.3.patch',
57
+ },
53
58
  });
54
59
  const devenvPath = join(root, 'tooling/direnv/devenv.nix');
55
60
  await mkdir(dirname(devenvPath), { recursive: true });
@@ -76,22 +81,104 @@ describe('tool configuration validation', () => {
76
81
  expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
77
82
  expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
78
83
  expect(rootPackage.devDependencies.nx).toBe('23.1.0');
79
- expect(rootPackage.devDependencies.ttsc).toBe('0.19.3');
84
+ expect(rootPackage.devDependencies.ttsc).toBe('0.25.0');
80
85
  expect(rootPackage.devDependencies.typescript).toBe('^6.0.3');
81
86
  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');
87
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
83
88
  expect(rootPackage.workspaces).toContain('tooling');
84
89
  expect(toolingPackage.name).toBe('@smoothbricks/tooling');
85
90
  expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
86
91
  expect(devenv).toContain('nodejs_latest');
87
92
  expect(devenv).toContain('coreutils');
88
93
  expect(devenv).toContain('git-format-staged');
94
+ expect(devenv).toContain('go # Builds ttsc source plugins');
95
+ expect(devenv).not.toContain('sccache');
96
+ } finally {
97
+ await rm(root, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ it('requires Go for managed TypeScript source-plugin builds without forcing Rust tools', async () => {
102
+ const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
103
+ try {
104
+ const devenvPath = join(root, 'tooling/direnv/devenv.nix');
105
+ await mkdir(dirname(devenvPath), { recursive: true });
106
+ await writeFile(
107
+ devenvPath,
108
+ `{
109
+ pkgs,
110
+ ...
111
+ }: {
112
+ packages = with pkgs; [
113
+ nodejs_latest
114
+ bun
115
+ git
116
+ git-format-staged
117
+ jq
118
+ alejandra
119
+ coreutils
120
+ gnutar
121
+ ];
122
+ }
123
+ `,
124
+ );
125
+
126
+ expect(validateDevenvPackages(root)).toBe(1);
127
+ applyDevenvPackageDefaults(root);
128
+ expect(validateDevenvPackages(root)).toBe(0);
129
+ const devenv = await readFile(devenvPath, 'utf8');
130
+ expect(devenv).toContain('go # Builds ttsc source plugins');
131
+ expect(devenv).not.toContain('sccache');
89
132
  } finally {
90
133
  await rm(root, { recursive: true, force: true });
91
134
  }
92
135
  });
93
136
 
94
- it('accepts newer dependency versions above the policy floor', async () => {
137
+ it('requires Rust compiler helpers only when the workspace contains Cargo manifests', async () => {
138
+ const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
139
+ try {
140
+ const crateRoot = join(root, 'packages/native');
141
+ await mkdir(crateRoot, { recursive: true });
142
+ await writeFile(join(crateRoot, 'Cargo.toml'), '[package]\nname = "native"\nversion = "0.0.0"\n');
143
+ const devenvPath = join(root, 'tooling/direnv/devenv.nix');
144
+ await mkdir(dirname(devenvPath), { recursive: true });
145
+ await writeFile(
146
+ devenvPath,
147
+ `{
148
+ pkgs,
149
+ lib,
150
+ ...
151
+ }: {
152
+ packages = with pkgs; [
153
+ nodejs_latest
154
+ bun
155
+ git
156
+ git-format-staged
157
+ jq
158
+ alejandra
159
+ coreutils
160
+ gnutar
161
+ go
162
+ stdenv.cc.cc.lib
163
+ ];
164
+ }
165
+ `,
166
+ );
167
+
168
+ expect(validateDevenvPackages(root)).toBe(2);
169
+ applyDevenvPackageDefaults(root);
170
+ expect(validateDevenvPackages(root)).toBe(0);
171
+ const devenv = await readFile(devenvPath, 'utf8');
172
+ expect(devenv).toContain('sccache # Rust compiler wrapper and cache');
173
+ expect(devenv).toContain('++ lib.optionals pkgs.stdenv.isLinux [');
174
+ expect(devenv).toContain('pkgs.stdenv.cc');
175
+ expect(devenv).toContain('stdenv.cc.cc.lib');
176
+ } finally {
177
+ await rm(root, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ it('accepts newer dependencies after removing even an empty obsolete ttsc patch key', async () => {
95
182
  const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
96
183
  try {
97
184
  await writeJson(join(root, 'package.json'), {
@@ -108,12 +195,12 @@ describe('tool configuration validation', () => {
108
195
  'eslint-stdout': await currentEslintStdoutRange(),
109
196
  nx: '24.0.0',
110
197
  prettier: '^3.7.0',
111
- ttsc: '0.19.3',
198
+ ttsc: '0.25.0',
112
199
  typescript: '^6.0.0',
113
200
  '@typescript/native': 'npm:typescript@^7.0.2',
114
201
  },
115
202
  patchedDependencies: {
116
- 'ttsc@0.19.3': 'patches/ttsc@0.19.3.patch',
203
+ 'ttsc@0.19.3': '',
117
204
  },
118
205
  });
119
206
  await writeJson(join(root, 'tooling/package.json'), {
@@ -138,12 +225,17 @@ describe('tool configuration validation', () => {
138
225
  alejandra
139
226
  coreutils
140
227
  gnutar
228
+ go
141
229
  ];
142
230
  }
143
231
  `,
144
232
  );
145
233
 
234
+ expect(await validateToolConfig(root)).toBe(1);
235
+ await applyToolConfigDefaults(root);
146
236
  expect(await validateToolConfig(root)).toBe(0);
237
+ const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
238
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
147
239
  } finally {
148
240
  await rm(root, { recursive: true, force: true });
149
241
  }
@@ -210,7 +302,7 @@ describe('tool configuration validation', () => {
210
302
  eslint: '^10.0.0',
211
303
  nx: '24.0.0',
212
304
  prettier: '^3.7.0',
213
- ttsc: '0.19.3',
305
+ ttsc: '0.25.0',
214
306
  typescript: '^6.0.0',
215
307
  },
216
308
  });
@@ -224,7 +316,7 @@ describe('tool configuration validation', () => {
224
316
  expect(rootPackage.devDependencies['eslint-stdout']).toBe(await currentEslintStdoutRange());
225
317
  expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe(await currentNxPluginRange());
226
318
  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');
319
+ expect(rootPackage.patchedDependencies?.['ttsc@0.19.3']).toBeUndefined();
228
320
  expect(toolingPackage.name).toBe('@fixture/tooling');
229
321
  expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(await currentCliRange());
230
322
  } finally {
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import typia from 'typia';
4
4
  import { cliPackageVersion, isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
@@ -56,8 +56,7 @@ const rootDevDependencies: RequiredDependency[] = [
56
56
  },
57
57
  { name: 'nx', fallbackVersion: '23.1.0', minimumVersion: '23.1.0' },
58
58
  { name: 'prettier', fallbackVersion: '^3.6.1', minimumVersion: '3.6.0', 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
+ { name: 'ttsc', fallbackVersion: '0.25.0' },
61
60
  // Nx and typescript-eslint still load the TypeScript JS API (6.x).
62
61
  // Compilation is exclusively delegated to ttsc by the Nx plugin targets.
63
62
  { name: 'typescript', fallbackVersion: '^6.0.3', minimumVersion: '6.0.0', prefix: '^' },
@@ -68,15 +67,26 @@ const rootDevDependencies: RequiredDependency[] = [
68
67
 
69
68
  const cliPackageName = '@smoothbricks/cli';
70
69
 
71
- const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejandra', 'coreutils', 'gnutar'];
70
+ const requiredDevenvPackages = ['bun', 'git', 'git-format-staged', 'jq', 'alejandra', 'coreutils', 'gnutar', 'go'];
72
71
  // Any explicit nodejs provider is fine — the pinned major is the repo's choice
73
72
  // (Lambda parity, org convergence, …). Whether the pins in package.json agree
74
73
  // with the runtime is validated against the live PATH (validateRootRuntimeVersions),
75
74
  // never against a version template here.
76
75
  const nodePackagePattern = /(^|\s)nodejs(_\d+|_latest)?(\s|#|$)/m;
77
-
78
- const TTSC_PATCHED_DEPENDENCY_KEY = 'ttsc@0.19.3';
79
- const TTSC_PATCH_PATH = 'patches/ttsc@0.19.3.patch';
76
+ const requiredRustDevenvPackages = ['sccache'];
77
+ const linuxCompilerPackage = 'pkgs.stdenv.cc';
78
+ const ignoredNativeManifestDirectories = new Set([
79
+ '.devenv',
80
+ '.direnv',
81
+ '.git',
82
+ '.nx',
83
+ 'dist',
84
+ 'node_modules',
85
+ 'target',
86
+ 'vendor',
87
+ ]);
88
+
89
+ const obsoleteTtscPatchedDependencyKey = 'ttsc@0.19.3';
80
90
 
81
91
  export async function applyToolConfigDefaults(root: string): Promise<void> {
82
92
  const context = await readToolContext(root);
@@ -89,7 +99,7 @@ export async function validateToolConfig(root: string): Promise<number> {
89
99
  const context = await readToolContext(root);
90
100
  return (
91
101
  validateRootDevDependencies(context.policy, context.rootPackage) +
92
- validateTtscPatchedDependency(context.rootPackage) +
102
+ validateObsoleteTtscPatchRemoved(context.rootPackage) +
93
103
  validateToolingPackage(root, context.policy) +
94
104
  validateToolingWorkspace(context.rootPackage) +
95
105
  validateDevenvPackages(root)
@@ -113,7 +123,7 @@ export async function applyRootDevDependencyDefaults(root: string, context: Tool
113
123
  if (delete devDependencies['@smoothbricks/cli']) {
114
124
  changed = true;
115
125
  }
116
- changed = ensureTtscPatchedDependency(pkg) || changed;
126
+ changed = removeObsoleteTtscPatch(pkg) || changed;
117
127
  if (changed) {
118
128
  writeJsonObject(join(root, 'package.json'), pkg);
119
129
  console.log('updated package.json workspace tool dependencies');
@@ -129,7 +139,6 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
129
139
  }
130
140
  let dependencyChanged = false;
131
141
  let workspaceChanged = false;
132
- let patchChanged = false;
133
142
  const devDependencies = ensureDependencyMap(pkg, 'devDependencies');
134
143
  for (const dependency of rootDevDependencies) {
135
144
  const current = devDependencies[dependency.name];
@@ -142,8 +151,8 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
142
151
  dependencyChanged = true;
143
152
  }
144
153
  workspaceChanged = addWorkspacePattern(pkg, 'tooling');
145
- patchChanged = ensureTtscPatchedDependency(pkg);
146
- if (dependencyChanged || workspaceChanged || patchChanged) {
154
+ dependencyChanged = removeObsoleteTtscPatch(pkg) || dependencyChanged;
155
+ if (dependencyChanged || workspaceChanged) {
147
156
  writeJsonObject(join(root, 'package.json'), pkg);
148
157
  }
149
158
  console.log(
@@ -156,11 +165,6 @@ async function applyRootPackageToolDefaults(root: string, context: ToolContext):
156
165
  ? 'updated package.json tooling workspace'
157
166
  : 'unchanged package.json tooling workspace',
158
167
  );
159
- console.log(
160
- patchChanged
161
- ? 'updated package.json patchedDependencies for ttsc'
162
- : 'unchanged package.json patchedDependencies for ttsc',
163
- );
164
168
  }
165
169
 
166
170
  export function applyToolingPackageDefaults(root: string, policy: ToolPolicy): void {
@@ -209,7 +213,9 @@ export function applyDevenvPackageDefaults(root: string): void {
209
213
  changed = next !== content || changed;
210
214
  content = next;
211
215
  }
212
- for (const name of requiredDevenvPackages) {
216
+ const compilesRust = workspaceCompilesRust(root);
217
+ const requiredPackages = [...requiredDevenvPackages, ...(compilesRust ? requiredRustDevenvPackages : [])];
218
+ for (const name of requiredPackages) {
213
219
  if (hasNixPackage(content, name)) {
214
220
  continue;
215
221
  }
@@ -217,6 +223,11 @@ export function applyDevenvPackageDefaults(root: string): void {
217
223
  changed = next !== content || changed;
218
224
  content = next;
219
225
  }
226
+ if (compilesRust && !hasLinuxCompilerPackage(content)) {
227
+ const next = addLinuxCompilerPackage(content);
228
+ changed = next !== content || changed;
229
+ content = next;
230
+ }
220
231
  if (changed) {
221
232
  writeFileSync(path, content);
222
233
  console.log('updated tooling/direnv/devenv.nix packages');
@@ -252,22 +263,25 @@ export function validateRootDevDependencies(policy: ToolPolicy, rootPackage: Pac
252
263
  return failures;
253
264
  }
254
265
 
255
- function ensureTtscPatchedDependency(pkg: PackageJson): boolean {
256
- const patched = ensureDependencyMap(pkg, 'patchedDependencies');
257
- return setStringProperty(patched, TTSC_PATCHED_DEPENDENCY_KEY, TTSC_PATCH_PATH);
266
+ function removeObsoleteTtscPatch(pkg: PackageJson): boolean {
267
+ const patchedDependencies = pkg.patchedDependencies;
268
+ if (!patchedDependencies || !(obsoleteTtscPatchedDependencyKey in patchedDependencies)) {
269
+ return false;
270
+ }
271
+ delete patchedDependencies[obsoleteTtscPatchedDependencyKey];
272
+ if (Object.keys(patchedDependencies).length === 0) {
273
+ delete pkg.patchedDependencies;
274
+ }
275
+ return true;
258
276
  }
259
277
 
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) {
278
+ function validateObsoleteTtscPatchRemoved(rootPackage: PackageJson | null): number {
279
+ const patchedDependencies = rootPackage?.patchedDependencies;
280
+ if (!patchedDependencies || !(obsoleteTtscPatchedDependencyKey in patchedDependencies)) {
266
281
  return 0;
267
282
  }
268
283
  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)'),
284
+ `package.json patchedDependencies.${obsoleteTtscPatchedDependencyKey} must be removed; ttsc 0.25.0 emits typia declarations without the obsolete patch`,
271
285
  );
272
286
  return 1;
273
287
  }
@@ -321,12 +335,20 @@ export function validateDevenvPackages(root: string): number {
321
335
  );
322
336
  failures++;
323
337
  }
324
- for (const name of requiredDevenvPackages) {
338
+ const compilesRust = workspaceCompilesRust(root);
339
+ const requiredPackages = [...requiredDevenvPackages, ...(compilesRust ? requiredRustDevenvPackages : [])];
340
+ for (const name of requiredPackages) {
325
341
  if (!hasNixPackage(content, name)) {
326
342
  console.error(`tooling/direnv/devenv.nix packages must include ${name}`);
327
343
  failures++;
328
344
  }
329
345
  }
346
+ if (compilesRust && !hasLinuxCompilerPackage(content)) {
347
+ console.error(
348
+ 'tooling/direnv/devenv.nix packages must include pkgs.stdenv.cc in a Linux-only package block for Rust builds',
349
+ );
350
+ failures++;
351
+ }
330
352
  return failures;
331
353
  }
332
354
 
@@ -335,7 +357,9 @@ function addNixPackage(content: string, name: string, comment: string): string {
335
357
  if (hasNixPackage(content, name)) {
336
358
  return content;
337
359
  }
338
- const packagesStart = content.indexOf(' packages = with pkgs; [');
360
+ const directPackagesStart = content.indexOf(' packages = with pkgs; [');
361
+ const groupedPackagesStart = content.indexOf(' (with pkgs; [');
362
+ const packagesStart = directPackagesStart === -1 ? groupedPackagesStart : directPackagesStart;
339
363
  if (packagesStart === -1) {
340
364
  return content;
341
365
  }
@@ -347,6 +371,60 @@ function hasNixPackage(content: string, name: string): boolean {
347
371
  return new RegExp(`(^|\\s)${escapeRegex(name)}(\\s|#|$)`, 'm').test(content);
348
372
  }
349
373
 
374
+ function workspaceCompilesRust(root: string): boolean {
375
+ return containsCargoManifest(root);
376
+ }
377
+
378
+ function containsCargoManifest(directory: string): boolean {
379
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
380
+ if (entry.isFile() && entry.name === 'Cargo.toml') {
381
+ return true;
382
+ }
383
+ if (
384
+ entry.isDirectory() &&
385
+ entry.name.startsWith('.') === false &&
386
+ ignoredNativeManifestDirectories.has(entry.name) === false &&
387
+ containsCargoManifest(join(directory, entry.name))
388
+ ) {
389
+ return true;
390
+ }
391
+ }
392
+ return false;
393
+ }
394
+
395
+ function hasLinuxCompilerPackage(content: string): boolean {
396
+ const linuxPackages = /\+\+\s+lib\.optionals\s+pkgs\.stdenv\.isLinux\s+\[([\s\S]*?)\]/g;
397
+ return [...content.matchAll(linuxPackages)].some((match) => hasNixPackage(match[1] ?? '', linuxCompilerPackage));
398
+ }
399
+
400
+ function addLinuxCompilerPackage(content: string): string {
401
+ if (hasLinuxCompilerPackage(content)) {
402
+ return content;
403
+ }
404
+ const directStart = content.indexOf(' packages = with pkgs; [');
405
+ if (directStart !== -1) {
406
+ const listEnd = content.indexOf('\n ];', directStart);
407
+ if (listEnd === -1) {
408
+ return content;
409
+ }
410
+ const listBody = content.slice(directStart + ' packages = with pkgs; ['.length, listEnd);
411
+ const replacement = ` packages =\n (with pkgs; [${listBody}\n ])\n ++ lib.optionals pkgs.stdenv.isLinux [\n ${linuxCompilerPackage}\n ];`;
412
+ return `${content.slice(0, directStart)}${replacement}${content.slice(listEnd + '\n ];'.length)}`;
413
+ }
414
+
415
+ const packagesStart = content.indexOf(' packages =');
416
+ const nextOption = content.indexOf('\n\n ', packagesStart);
417
+ if (packagesStart === -1 || nextOption === -1) {
418
+ return content;
419
+ }
420
+ const terminator = content.lastIndexOf(';', nextOption);
421
+ if (terminator < packagesStart) {
422
+ return content;
423
+ }
424
+ const linuxPackages = `\n ++ lib.optionals pkgs.stdenv.isLinux [\n ${linuxCompilerPackage}\n ]`;
425
+ return `${content.slice(0, terminator)}${linuxPackages}${content.slice(terminator)}`;
426
+ }
427
+
350
428
  function nixPackageComment(name: string): string {
351
429
  if (name === 'coreutils') {
352
430
  return '# Provides fmt for commit message wrapping';
@@ -357,6 +435,12 @@ function nixPackageComment(name: string): string {
357
435
  if (name === 'git') {
358
436
  return '# Git hooks and repository inspection';
359
437
  }
438
+ if (name === 'go') {
439
+ return '# Builds ttsc source plugins';
440
+ }
441
+ if (name === 'sccache') {
442
+ return '# Rust compiler wrapper and cache';
443
+ }
360
444
  return '';
361
445
  }
362
446