@smoothbricks/cli 0.1.1 → 0.3.0

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 (131) hide show
  1. package/README.md +161 -32
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +110 -10
  4. package/dist/devenv/index.d.ts +10 -0
  5. package/dist/devenv/index.d.ts.map +1 -0
  6. package/dist/devenv/index.js +27 -0
  7. package/dist/generate/index.d.ts +20 -0
  8. package/dist/generate/index.d.ts.map +1 -0
  9. package/dist/generate/index.js +47 -0
  10. package/dist/lib/cli-package.d.ts +4 -0
  11. package/dist/lib/cli-package.d.ts.map +1 -0
  12. package/dist/lib/cli-package.js +15 -0
  13. package/dist/lib/run.d.ts +6 -0
  14. package/dist/lib/run.d.ts.map +1 -1
  15. package/dist/lib/run.js +32 -0
  16. package/dist/lib/workspace.d.ts +25 -1
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +79 -17
  19. package/dist/monorepo/commit-msg.d.ts +8 -1
  20. package/dist/monorepo/commit-msg.d.ts.map +1 -1
  21. package/dist/monorepo/commit-msg.js +85 -7
  22. package/dist/monorepo/index.d.ts +19 -2
  23. package/dist/monorepo/index.d.ts.map +1 -1
  24. package/dist/monorepo/index.js +104 -16
  25. package/dist/monorepo/lockfile.d.ts.map +1 -1
  26. package/dist/monorepo/lockfile.js +9 -0
  27. package/dist/monorepo/managed-files.js +1 -1
  28. package/dist/monorepo/nx-sync.d.ts +2 -1
  29. package/dist/monorepo/nx-sync.d.ts.map +1 -1
  30. package/dist/monorepo/nx-sync.js +8 -2
  31. package/dist/monorepo/package-hygiene.d.ts +6 -2
  32. package/dist/monorepo/package-hygiene.d.ts.map +1 -1
  33. package/dist/monorepo/package-hygiene.js +12 -4
  34. package/dist/monorepo/package-policy.d.ts +13 -4
  35. package/dist/monorepo/package-policy.d.ts.map +1 -1
  36. package/dist/monorepo/package-policy.js +208 -113
  37. package/dist/monorepo/packed-package.d.ts +3 -0
  38. package/dist/monorepo/packed-package.d.ts.map +1 -1
  39. package/dist/monorepo/packed-package.js +68 -36
  40. package/dist/monorepo/packs/index.d.ts +20 -1
  41. package/dist/monorepo/packs/index.d.ts.map +1 -1
  42. package/dist/monorepo/packs/index.js +238 -19
  43. package/dist/monorepo/publish-workflow.d.ts +0 -4
  44. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  45. package/dist/monorepo/publish-workflow.js +7 -15
  46. package/dist/monorepo/tool-validation.d.ts +22 -0
  47. package/dist/monorepo/tool-validation.d.ts.map +1 -0
  48. package/dist/monorepo/tool-validation.js +399 -0
  49. package/dist/nx/index.d.ts +25 -0
  50. package/dist/nx/index.d.ts.map +1 -0
  51. package/dist/nx/index.js +112 -0
  52. package/dist/release/bootstrap-npm-packages.d.ts +19 -0
  53. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
  54. package/dist/release/bootstrap-npm-packages.js +58 -0
  55. package/dist/release/candidates.d.ts +11 -0
  56. package/dist/release/candidates.d.ts.map +1 -0
  57. package/dist/release/candidates.js +181 -0
  58. package/dist/release/core.d.ts +4 -1
  59. package/dist/release/core.d.ts.map +1 -1
  60. package/dist/release/core.js +76 -9
  61. package/dist/release/github-release.d.ts +1 -0
  62. package/dist/release/github-release.d.ts.map +1 -1
  63. package/dist/release/github-release.js +3 -3
  64. package/dist/release/index.d.ts +32 -1
  65. package/dist/release/index.d.ts.map +1 -1
  66. package/dist/release/index.js +488 -64
  67. package/dist/release/npm-auth.d.ts +0 -1
  68. package/dist/release/npm-auth.d.ts.map +1 -1
  69. package/dist/release/npm-auth.js +4 -16
  70. package/dist/release/orchestration.d.ts +11 -2
  71. package/dist/release/orchestration.d.ts.map +1 -1
  72. package/dist/release/orchestration.js +41 -3
  73. package/dist/release/publish-plan.js +1 -1
  74. package/managed/raw/git-format-staged.yml +54 -0
  75. package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
  76. package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
  77. package/managed/templates/github/workflows/ci.yml +4 -2
  78. package/package.json +15 -8
  79. package/src/cli.ts +153 -22
  80. package/src/devenv/index.test.ts +49 -0
  81. package/src/devenv/index.ts +38 -0
  82. package/src/generate/index.ts +92 -0
  83. package/src/lib/cli-package.ts +18 -0
  84. package/src/lib/run.ts +44 -0
  85. package/src/lib/workspace.ts +112 -17
  86. package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
  87. package/src/monorepo/commit-msg.test.ts +45 -0
  88. package/src/monorepo/commit-msg.ts +107 -7
  89. package/src/monorepo/index.ts +142 -16
  90. package/src/monorepo/lockfile.ts +9 -0
  91. package/src/monorepo/managed-files.ts +1 -1
  92. package/src/monorepo/nx-sync.ts +9 -2
  93. package/src/monorepo/package-hygiene.test.ts +34 -0
  94. package/src/monorepo/package-hygiene.ts +26 -4
  95. package/src/monorepo/package-policy.test.ts +1130 -0
  96. package/src/monorepo/package-policy.ts +237 -114
  97. package/src/monorepo/packed-package.ts +89 -37
  98. package/src/monorepo/packs/index.test.ts +189 -0
  99. package/src/monorepo/packs/index.ts +311 -20
  100. package/src/monorepo/publish-workflow.ts +10 -19
  101. package/src/monorepo/setup-test-tracing.test.ts +120 -0
  102. package/src/monorepo/tool-validation.test.ts +261 -0
  103. package/src/monorepo/tool-validation.ts +462 -0
  104. package/src/nx/index.test.ts +73 -0
  105. package/src/nx/index.ts +139 -0
  106. package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
  107. package/src/release/__tests__/candidates.test.ts +262 -0
  108. package/src/release/__tests__/core-properties.test.ts +6 -6
  109. package/src/release/__tests__/core-scenarios.test.ts +27 -20
  110. package/src/release/__tests__/core.test.ts +52 -5
  111. package/src/release/__tests__/fixture-repo.test.ts +90 -31
  112. package/src/release/__tests__/github-release.test.ts +22 -24
  113. package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
  114. package/src/release/__tests__/npm-auth.test.ts +12 -21
  115. package/src/release/__tests__/orchestration.test.ts +142 -12
  116. package/src/release/__tests__/publish-plan.test.ts +9 -3
  117. package/src/release/__tests__/retag-unpublished.test.ts +10 -10
  118. package/src/release/__tests__/trust-publisher.test.ts +165 -0
  119. package/src/release/bootstrap-npm-packages.ts +88 -0
  120. package/src/release/candidates.ts +225 -0
  121. package/src/release/core.ts +96 -10
  122. package/src/release/github-release.ts +3 -3
  123. package/src/release/index.ts +658 -74
  124. package/src/release/npm-auth.ts +4 -33
  125. package/src/release/orchestration.ts +60 -5
  126. package/src/release/publish-plan.ts +1 -1
  127. package/dist/nx-version-actions.cjs +0 -25
  128. package/dist/nx-version-actions.d.cts +0 -6
  129. package/dist/nx-version-actions.d.cts.map +0 -1
  130. package/managed/raw/.git-format-staged.yml +0 -47
  131. package/src/nx-version-actions.cts +0 -36
@@ -1,3 +1,5 @@
1
+ import { isSmoothBricksCodebasePackageName } from '../lib/cli-package.js';
2
+
1
3
  export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
2
4
  export type PublishWorkflowCondition = 'version-mode-not-none' | 'failure' | 'always';
3
5
  export type PublishWorkflowNxTarget = 'build' | 'lint' | 'test';
@@ -26,7 +28,6 @@ export interface PublishWorkflowStep {
26
28
  id?: string;
27
29
  condition?: PublishWorkflowCondition;
28
30
  nxTarget?: PublishWorkflowNxTarget;
29
- needsNodeAuthToken?: boolean;
30
31
  }
31
32
 
32
33
  export interface PublishWorkflowDefinition {
@@ -57,19 +58,18 @@ export interface PublishWorkflowCallbacks {
57
58
  setupDevenv(): Promise<PublishWorkflowSetupOutputs>;
58
59
  configureReleaseAuthor(): Promise<void>;
59
60
  buildSelfHostedCli(): Promise<void>;
60
- repairPendingReleases(input: { dryRun: boolean; nodeAuthToken: boolean }): Promise<void>;
61
+ repairPendingReleases(input: { dryRun: boolean }): Promise<void>;
61
62
  versionRelease(input: { bump: PublishWorkflowBump; dryRun: boolean }): Promise<PublishWorkflowVersionOutputs>;
62
63
  checkManagedMonorepoFiles(): Promise<void>;
63
64
  nxRunMany(input: { target: PublishWorkflowNxTarget; projects: string[] }): Promise<void>;
64
65
  uploadTraceDbs(): Promise<void>;
65
66
  validateMonorepoConfig(): Promise<void>;
66
- publishRelease(input: { bump: PublishWorkflowBump; dryRun: boolean; nodeAuthToken: boolean }): Promise<void>;
67
+ publishRelease(input: { bump: PublishWorkflowBump; dryRun: boolean }): Promise<void>;
67
68
  saveNixDevenv(input: PublishWorkflowSetupOutputs): Promise<void>;
68
69
  }
69
70
 
70
71
  export interface PublishWorkflowRunContext {
71
72
  inputs: PublishWorkflowInputs;
72
- nodeAuthToken: boolean;
73
73
  callbacks: PublishWorkflowCallbacks;
74
74
  }
75
75
 
@@ -87,7 +87,7 @@ export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions
87
87
  { kind: PublishWorkflowStepKind.SetupDevenv, name: '🧱 Setup Nix/devenv', id: 'setup' },
88
88
  { kind: PublishWorkflowStepKind.ConfigureReleaseAuthor, name: '🤖 Configure release author' },
89
89
  ];
90
- if (options.repoName === '@smoothbricks/codebase') {
90
+ if (isSmoothBricksCodebasePackageName(options.repoName)) {
91
91
  setupSteps.push({ kind: PublishWorkflowStepKind.BuildSelfHostedCli, name: '🏗️ Build self-hosted smoo' });
92
92
  }
93
93
  return {
@@ -96,7 +96,6 @@ export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions
96
96
  {
97
97
  kind: PublishWorkflowStepKind.RepairPendingReleases,
98
98
  name: '🧯 Repair pending releases',
99
- needsNodeAuthToken: true,
100
99
  },
101
100
  { kind: PublishWorkflowStepKind.VersionRelease, name: '🏷️ Version release', id: 'version' },
102
101
  {
@@ -131,7 +130,6 @@ export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions
131
130
  {
132
131
  kind: PublishWorkflowStepKind.PublishRelease,
133
132
  name: `📦 Publish release (${versionMode})`,
134
- needsNodeAuthToken: true,
135
133
  },
136
134
  {
137
135
  kind: PublishWorkflowStepKind.SaveNixDevenv,
@@ -175,7 +173,6 @@ export async function runPublishWorkflow(
175
173
  case PublishWorkflowStepKind.RepairPendingReleases:
176
174
  await context.callbacks.repairPendingReleases({
177
175
  dryRun: context.inputs.dryRun,
178
- nodeAuthToken: context.nodeAuthToken,
179
176
  });
180
177
  break;
181
178
  case PublishWorkflowStepKind.VersionRelease:
@@ -202,7 +199,6 @@ export async function runPublishWorkflow(
202
199
  await context.callbacks.publishRelease({
203
200
  bump: context.inputs.bump,
204
201
  dryRun: context.inputs.dryRun,
205
- nodeAuthToken: context.nodeAuthToken,
206
202
  });
207
203
  break;
208
204
  case PublishWorkflowStepKind.SaveNixDevenv:
@@ -345,15 +341,12 @@ function yamlLinesForStep(step: PublishWorkflowStep): string[] {
345
341
  case PublishWorkflowStepKind.BuildSelfHostedCli:
346
342
  return [
347
343
  ` - name: ${step.name}`,
348
- ' # SmoothBricks self-hosts smoo from source, but Nx release loads',
349
- ' # @smoothbricks/cli/nx-version-actions through the built package export.',
350
- ' run: nx build @smoothbricks/cli',
344
+ ' # SmoothBricks self-hosts smoo from source for release commands.',
345
+ ' run: nx build cli',
351
346
  ];
352
347
  case PublishWorkflowStepKind.RepairPendingReleases:
353
348
  return [
354
349
  ` - name: ${step.name}`,
355
- ' env:',
356
- ` NODE_AUTH_TOKEN: ${githubExpression('secrets.NPM_TOKEN')}`,
357
350
  ` run: smoo release repair-pending --dry-run "${githubExpression('inputs.dry_run')}"`,
358
351
  ];
359
352
  case PublishWorkflowStepKind.VersionRelease:
@@ -385,7 +378,7 @@ function yamlLinesForStep(step: PublishWorkflowStep): string[] {
385
378
  return [
386
379
  ` - name: ${step.name}`,
387
380
  ` if: ${githubExpression('failure()')}`,
388
- ' uses: actions/upload-artifact@v4',
381
+ ' uses: actions/upload-artifact@v7.0.1',
389
382
  ' with:',
390
383
  ` name: trace-results-${githubExpression('github.run_id')}`,
391
384
  ' path: packages/*/.trace-results.db',
@@ -399,10 +392,8 @@ function yamlLinesForStep(step: PublishWorkflowStep): string[] {
399
392
  return [
400
393
  ` - name: ${step.name}`,
401
394
  ' # smoo packs with Bun, then publishes tarballs with npm. Existing',
402
- ' # packages use trusted publishing/OIDC; first publishes use NODE_AUTH_TOKEN',
403
- ' # because npm trust can only be configured after a package exists.',
404
- ' env:',
405
- ` NODE_AUTH_TOKEN: ${githubExpression('secrets.NPM_TOKEN')}`,
395
+ ' # packages must already exist on npm and use trusted publishing/OIDC.',
396
+ ' # Missing package names are bootstrapped locally before trust setup.',
406
397
  ` run: smoo release publish --bump "${githubExpression('inputs.bump')}" --dry-run "${githubExpression('inputs.dry_run')}"`,
407
398
  ];
408
399
  case PublishWorkflowStepKind.SaveNixDevenv:
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { type SetupTestTracingShell, setupTestTracing } from './index.js';
6
+
7
+ describe('LMAO test tracing setup', () => {
8
+ it('runs the Bun test tracing generator for every workspace package', async () => {
9
+ await withWorkspace(async (root) => {
10
+ await writeWorkspacePackage(root, 'packages/a', '@scope/a', 'a');
11
+ await writeWorkspacePackage(root, 'packages/b', '@scope/b', 'b');
12
+ const shell = new RecordingSetupShell();
13
+
14
+ await setupTestTracing(root, { all: true }, shell);
15
+
16
+ expect(shell.runs).toEqual([generatorRun(root, 'a', '@scope/a'), generatorRun(root, 'b', '@scope/b')]);
17
+ });
18
+ });
19
+
20
+ it('selects packages by project name, package name, or package root', async () => {
21
+ await withWorkspace(async (root) => {
22
+ await writeWorkspacePackage(root, 'packages/a', '@scope/a', 'a');
23
+ await writeWorkspacePackage(root, 'packages/b', '@scope/b', 'b');
24
+ await writeWorkspacePackage(root, 'packages/c', '@scope/c', 'c');
25
+ const shell = new RecordingSetupShell();
26
+
27
+ await setupTestTracing(root, { projects: 'a,@scope/b,packages/c', opContextExport: 'customContext' }, shell);
28
+
29
+ expect(shell.runs).toEqual([
30
+ generatorRun(root, 'a', '@scope/a', 'customContext'),
31
+ generatorRun(root, 'b', '@scope/b', 'customContext'),
32
+ generatorRun(root, 'c', '@scope/c', 'customContext'),
33
+ ]);
34
+ });
35
+ });
36
+
37
+ it('prints generator invocations in dry-run mode without running Nx', async () => {
38
+ await withWorkspace(async (root) => {
39
+ await writeWorkspacePackage(root, 'packages/a', '@scope/a', 'a');
40
+ const shell = new RecordingSetupShell();
41
+
42
+ await setupTestTracing(root, { all: true, dryRun: true }, shell);
43
+
44
+ expect(shell.runs).toEqual([]);
45
+ expect(shell.logs).toEqual([
46
+ 'would run nx g @smoothbricks/nx-plugin:bun-test-tracing --project a --opContextModule @scope/a --opContextExport opContext --tracerModule @smoothbricks/lmao/testing/bun',
47
+ ]);
48
+ });
49
+ });
50
+ });
51
+
52
+ class RecordingSetupShell implements SetupTestTracingShell {
53
+ readonly runs: { command: string; args: string[]; cwd: string }[] = [];
54
+ readonly logs: string[] = [];
55
+
56
+ async run(command: string, args: string[], cwd: string): Promise<void> {
57
+ this.runs.push({ command, args, cwd });
58
+ }
59
+
60
+ log(message: string): void {
61
+ this.logs.push(message);
62
+ }
63
+ }
64
+
65
+ async function withWorkspace(run: (root: string) => Promise<void>): Promise<void> {
66
+ const root = await mkdtemp(join(tmpdir(), 'smoo-test-tracing-'));
67
+ try {
68
+ await writeJson(join(root, 'package.json'), {
69
+ name: '@scope/workspace',
70
+ version: '0.0.0',
71
+ private: true,
72
+ workspaces: ['packages/*'],
73
+ });
74
+ await run(root);
75
+ } finally {
76
+ await rm(root, { recursive: true, force: true });
77
+ }
78
+ }
79
+
80
+ async function writeWorkspacePackage(
81
+ root: string,
82
+ packagePath: string,
83
+ name: string,
84
+ projectName: string,
85
+ ): Promise<void> {
86
+ await mkdir(join(root, packagePath), { recursive: true });
87
+ await writeJson(join(root, packagePath, 'package.json'), {
88
+ name,
89
+ version: '1.0.0',
90
+ nx: { name: projectName },
91
+ });
92
+ }
93
+
94
+ async function writeJson(path: string, value: unknown): Promise<void> {
95
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
96
+ }
97
+
98
+ function generatorRun(
99
+ root: string,
100
+ project: string,
101
+ opContextModule: string,
102
+ opContextExport = 'opContext',
103
+ ): { command: string; args: string[]; cwd: string } {
104
+ return {
105
+ command: 'nx',
106
+ args: [
107
+ 'g',
108
+ '@smoothbricks/nx-plugin:bun-test-tracing',
109
+ '--project',
110
+ project,
111
+ '--opContextModule',
112
+ opContextModule,
113
+ '--opContextExport',
114
+ opContextExport,
115
+ '--tracerModule',
116
+ '@smoothbricks/lmao/testing/bun',
117
+ ],
118
+ cwd: root,
119
+ };
120
+ }
@@ -0,0 +1,261 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import {
6
+ applyRootDevDependencyDefaults,
7
+ applyToolConfigDefaults,
8
+ applyToolingPackageDefaults,
9
+ readToolContext,
10
+ validateToolConfig,
11
+ } from './tool-validation.js';
12
+
13
+ const registryVersions: Record<string, string[]> = {
14
+ '@biomejs/biome': ['2.3.5'],
15
+ '@nx/js': ['22.5.4'],
16
+ '@smoothbricks/nx-plugin': ['0.0.1'],
17
+ eslint: ['9.39.1'],
18
+ 'eslint-stdout': ['1.1.1', '1.1.2'],
19
+ nx: ['22.5.4'],
20
+ prettier: ['3.6.1'],
21
+ typescript: ['5.9.3'],
22
+ };
23
+
24
+ const realFetch = globalThis.fetch;
25
+
26
+ describe('tool configuration validation', () => {
27
+ beforeEach(() => {
28
+ globalThis.fetch = mockRegistryFetch as typeof fetch;
29
+ });
30
+
31
+ afterEach(() => {
32
+ globalThis.fetch = realFetch;
33
+ });
34
+
35
+ it('fixes root, tooling, and devenv tool declarations', async () => {
36
+ const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
37
+ try {
38
+ await writeJson(join(root, 'package.json'), {
39
+ name: '@smoothbricks/codebase',
40
+ version: '0.0.0',
41
+ private: true,
42
+ devDependencies: {
43
+ '@smoothbricks/cli': 'workspace:*',
44
+ },
45
+ });
46
+ const devenvPath = join(root, 'tooling/direnv/devenv.nix');
47
+ await mkdir(dirname(devenvPath), { recursive: true });
48
+ await writeFile(
49
+ devenvPath,
50
+ `{
51
+ pkgs,
52
+ ...
53
+ }: {
54
+ packages = with pkgs; [
55
+ ];
56
+ }
57
+ `,
58
+ );
59
+
60
+ expect(validateToolConfig(root)).toBeGreaterThan(0);
61
+ await applyToolConfigDefaults(root);
62
+ expect(validateToolConfig(root)).toBe(0);
63
+
64
+ const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
65
+ const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
66
+ const devenv = await readFile(join(root, 'tooling/direnv/devenv.nix'), 'utf8');
67
+ expect(rootPackage.devDependencies['@smoothbricks/cli']).toBeUndefined();
68
+ expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe('workspace:*');
69
+ expect(rootPackage.devDependencies['eslint-stdout']).toBe('workspace:*');
70
+ expect(rootPackage.devDependencies.nx).toBe('22.5.4');
71
+ expect(rootPackage.workspaces).toContain('tooling');
72
+ expect(toolingPackage.name).toBe('@smoothbricks/tooling');
73
+ expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
74
+ expect(devenv).toContain('nodejs_latest');
75
+ expect(devenv).toContain('coreutils');
76
+ expect(devenv).toContain('git-format-staged');
77
+ } finally {
78
+ await rm(root, { recursive: true, force: true });
79
+ }
80
+ });
81
+
82
+ it('accepts newer dependency versions above the policy floor', async () => {
83
+ const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
84
+ try {
85
+ await writeJson(join(root, 'package.json'), {
86
+ name: '@fixture/app',
87
+ version: '0.0.0',
88
+ private: true,
89
+ workspaces: ['packages/*', 'tooling'],
90
+ devDependencies: {
91
+ '@biomejs/biome': '^3.0.0',
92
+ '@nx/js': '22.6.0',
93
+ '@smoothbricks/nx-plugin': '^0.1.0',
94
+ '@types/bun': '1.3.99',
95
+ eslint: '^10.0.0',
96
+ 'eslint-stdout': await currentEslintStdoutRange(),
97
+ nx: '23.0.0',
98
+ prettier: '^3.7.0',
99
+ typescript: '^6.0.0',
100
+ },
101
+ });
102
+ await writeJson(join(root, 'tooling/package.json'), {
103
+ name: '@fixture/tooling',
104
+ private: true,
105
+ dependencies: { '@smoothbricks/cli': await currentCliRange() },
106
+ });
107
+ const devenvPath = join(root, 'tooling/direnv/devenv.nix');
108
+ await mkdir(dirname(devenvPath), { recursive: true });
109
+ await writeFile(
110
+ devenvPath,
111
+ `{
112
+ pkgs,
113
+ ...
114
+ }: {
115
+ packages = with pkgs; [
116
+ nodejs_latest
117
+ bun
118
+ git
119
+ git-format-staged
120
+ jq
121
+ alejandra
122
+ coreutils
123
+ gnutar
124
+ ];
125
+ }
126
+ `,
127
+ );
128
+
129
+ expect(validateToolConfig(root)).toBe(0);
130
+ } finally {
131
+ await rm(root, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ it('uses workspace smoo in the SmoothBricks codebase repo', async () => {
136
+ const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
137
+ try {
138
+ await writeJson(join(root, 'package.json'), {
139
+ name: '@smoothbricks/codebase',
140
+ version: '0.0.0',
141
+ private: true,
142
+ workspaces: ['packages/*', 'tooling'],
143
+ });
144
+ applyToolingPackageDefaults(root, readToolContext(root).policy);
145
+
146
+ const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
147
+ expect(toolingPackage.name).toBe('@smoothbricks/tooling');
148
+ expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe('workspace:*');
149
+ } finally {
150
+ await rm(root, { recursive: true, force: true });
151
+ }
152
+ });
153
+
154
+ it('uses published smoo and eslint formatter ranges in user repos', async () => {
155
+ const root = await mkdtemp(join(tmpdir(), 'smoo-tool-validation-'));
156
+ try {
157
+ await writeJson(join(root, 'package.json'), {
158
+ name: '@fixture/app',
159
+ version: '0.0.0',
160
+ private: true,
161
+ workspaces: ['packages/*', 'tooling'],
162
+ devDependencies: {
163
+ '@biomejs/biome': '^3.0.0',
164
+ '@nx/js': '22.6.0',
165
+ eslint: '^10.0.0',
166
+ nx: '23.0.0',
167
+ prettier: '^3.7.0',
168
+ typescript: '^6.0.0',
169
+ },
170
+ });
171
+
172
+ const context = readToolContext(root);
173
+ await applyRootDevDependencyDefaults(root, context);
174
+ applyToolingPackageDefaults(root, context.policy);
175
+
176
+ const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
177
+ const toolingPackage = JSON.parse(await readFile(join(root, 'tooling/package.json'), 'utf8'));
178
+ expect(rootPackage.devDependencies['eslint-stdout']).toBe(await currentEslintStdoutRange());
179
+ expect(rootPackage.devDependencies['@smoothbricks/nx-plugin']).toBe(await currentNxPluginRange());
180
+ expect(toolingPackage.name).toBe('@fixture/tooling');
181
+ expect(toolingPackage.dependencies['@smoothbricks/cli']).toBe(await currentCliRange());
182
+ } finally {
183
+ await rm(root, { recursive: true, force: true });
184
+ }
185
+ });
186
+ });
187
+
188
+ async function currentCliRange(): Promise<string> {
189
+ const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
190
+ return `^${pkg.version}`;
191
+ }
192
+
193
+ async function currentEslintStdoutRange(): Promise<string> {
194
+ const pkg = JSON.parse(await readFile(new URL('../../../eslint-stdout/package.json', import.meta.url), 'utf8'));
195
+ return `^${latestPatchVersion('eslint-stdout', pkg.version) ?? pkg.version}`;
196
+ }
197
+
198
+ function mockRegistryFetch(input: Parameters<typeof fetch>[0]): Promise<Response> {
199
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
200
+ const packageName = decodeURIComponent(new URL(url).pathname.slice(1));
201
+ const versions = registryVersions[packageName];
202
+ if (!versions) {
203
+ return Promise.resolve(new Response('{}', { status: 404 }));
204
+ }
205
+ return Promise.resolve(
206
+ new Response(JSON.stringify({ versions: Object.fromEntries(versions.map((version) => [version, {}])) }), {
207
+ status: 200,
208
+ headers: { 'content-type': 'application/json' },
209
+ }),
210
+ );
211
+ }
212
+
213
+ function latestPatchVersion(packageName: string, version: string): string | null {
214
+ const [major, minor] = version.split('.');
215
+ if (!major || !minor) {
216
+ return null;
217
+ }
218
+ return (
219
+ (registryVersions[packageName] ?? [])
220
+ .filter((candidate) => candidate.startsWith(`${major}.${minor}.`))
221
+ .sort((left, right) => compareDotVersions(right, left))[0] ?? null
222
+ );
223
+ }
224
+
225
+ function compareDotVersions(left: string, right: string): number {
226
+ const leftParts = left.split('.').map(Number);
227
+ const rightParts = right.split('.').map(Number);
228
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
229
+ const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
230
+ if (diff !== 0) {
231
+ return diff;
232
+ }
233
+ }
234
+ return 0;
235
+ }
236
+
237
+ async function currentNxPluginRange(): Promise<string> {
238
+ return `^${latestRegistryVersion('@smoothbricks/nx-plugin')}`;
239
+ }
240
+
241
+ function latestRegistryVersion(packageName: string): string {
242
+ const versions = registryVersions[packageName];
243
+ if (!versions) {
244
+ throw new Error(`missing mocked registry versions for ${packageName}`);
245
+ }
246
+ let latest = versions[0];
247
+ for (const version of versions) {
248
+ if (!latest || compareDotVersions(version, latest) > 0) {
249
+ latest = version;
250
+ }
251
+ }
252
+ if (!latest) {
253
+ throw new Error(`empty mocked registry versions for ${packageName}`);
254
+ }
255
+ return latest;
256
+ }
257
+
258
+ async function writeJson(path: string, value: Record<string, unknown>): Promise<void> {
259
+ await mkdir(dirname(path), { recursive: true });
260
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
261
+ }