@smoothbricks/cli 0.1.1 → 0.2.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 (117) hide show
  1. package/README.md +155 -30
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +91 -7
  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/lib/cli-package.d.ts +4 -0
  8. package/dist/lib/cli-package.d.ts.map +1 -0
  9. package/dist/lib/cli-package.js +15 -0
  10. package/dist/lib/run.d.ts +5 -0
  11. package/dist/lib/run.d.ts.map +1 -1
  12. package/dist/lib/run.js +13 -0
  13. package/dist/lib/workspace.d.ts +15 -1
  14. package/dist/lib/workspace.d.ts.map +1 -1
  15. package/dist/lib/workspace.js +25 -2
  16. package/dist/monorepo/commit-msg.d.ts +8 -1
  17. package/dist/monorepo/commit-msg.d.ts.map +1 -1
  18. package/dist/monorepo/commit-msg.js +85 -7
  19. package/dist/monorepo/index.d.ts +19 -2
  20. package/dist/monorepo/index.d.ts.map +1 -1
  21. package/dist/monorepo/index.js +104 -16
  22. package/dist/monorepo/managed-files.js +1 -1
  23. package/dist/monorepo/nx-sync.d.ts +2 -1
  24. package/dist/monorepo/nx-sync.d.ts.map +1 -1
  25. package/dist/monorepo/nx-sync.js +8 -2
  26. package/dist/monorepo/package-hygiene.d.ts +6 -2
  27. package/dist/monorepo/package-hygiene.d.ts.map +1 -1
  28. package/dist/monorepo/package-hygiene.js +12 -4
  29. package/dist/monorepo/package-policy.d.ts +22 -2
  30. package/dist/monorepo/package-policy.d.ts.map +1 -1
  31. package/dist/monorepo/package-policy.js +1167 -8
  32. package/dist/monorepo/packed-package.d.ts +3 -0
  33. package/dist/monorepo/packed-package.d.ts.map +1 -1
  34. package/dist/monorepo/packed-package.js +68 -36
  35. package/dist/monorepo/packs/index.d.ts +20 -1
  36. package/dist/monorepo/packs/index.d.ts.map +1 -1
  37. package/dist/monorepo/packs/index.js +238 -19
  38. package/dist/monorepo/publish-workflow.d.ts +0 -4
  39. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  40. package/dist/monorepo/publish-workflow.js +5 -12
  41. package/dist/monorepo/tool-validation.d.ts +22 -0
  42. package/dist/monorepo/tool-validation.d.ts.map +1 -0
  43. package/dist/monorepo/tool-validation.js +399 -0
  44. package/dist/nx/index.d.ts +24 -0
  45. package/dist/nx/index.d.ts.map +1 -0
  46. package/dist/nx/index.js +94 -0
  47. package/dist/release/bootstrap-npm-packages.d.ts +19 -0
  48. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
  49. package/dist/release/bootstrap-npm-packages.js +58 -0
  50. package/dist/release/candidates.d.ts +11 -0
  51. package/dist/release/candidates.d.ts.map +1 -0
  52. package/dist/release/candidates.js +181 -0
  53. package/dist/release/core.d.ts +1 -0
  54. package/dist/release/core.d.ts.map +1 -1
  55. package/dist/release/github-release.js +2 -2
  56. package/dist/release/index.d.ts +25 -0
  57. package/dist/release/index.d.ts.map +1 -1
  58. package/dist/release/index.js +247 -49
  59. package/dist/release/npm-auth.d.ts +0 -1
  60. package/dist/release/npm-auth.d.ts.map +1 -1
  61. package/dist/release/npm-auth.js +4 -16
  62. package/dist/release/orchestration.d.ts +2 -1
  63. package/dist/release/orchestration.d.ts.map +1 -1
  64. package/dist/release/orchestration.js +9 -1
  65. package/dist/release/publish-plan.js +1 -1
  66. package/managed/raw/git-format-staged.yml +54 -0
  67. package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
  68. package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
  69. package/managed/templates/github/workflows/ci.yml +1 -0
  70. package/package.json +15 -3
  71. package/src/cli.ts +134 -20
  72. package/src/devenv/index.test.ts +49 -0
  73. package/src/devenv/index.ts +38 -0
  74. package/src/lib/cli-package.ts +18 -0
  75. package/src/lib/run.ts +19 -0
  76. package/src/lib/workspace.ts +41 -2
  77. package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
  78. package/src/monorepo/commit-msg.test.ts +45 -0
  79. package/src/monorepo/commit-msg.ts +107 -7
  80. package/src/monorepo/index.ts +142 -16
  81. package/src/monorepo/managed-files.ts +1 -1
  82. package/src/monorepo/nx-sync.ts +9 -2
  83. package/src/monorepo/package-hygiene.test.ts +34 -0
  84. package/src/monorepo/package-hygiene.ts +26 -4
  85. package/src/monorepo/package-policy.test.ts +902 -0
  86. package/src/monorepo/package-policy.ts +1393 -7
  87. package/src/monorepo/packed-package.ts +89 -37
  88. package/src/monorepo/packs/index.test.ts +189 -0
  89. package/src/monorepo/packs/index.ts +311 -20
  90. package/src/monorepo/publish-workflow.ts +8 -16
  91. package/src/monorepo/setup-test-tracing.test.ts +120 -0
  92. package/src/monorepo/tool-validation.test.ts +261 -0
  93. package/src/monorepo/tool-validation.ts +462 -0
  94. package/src/nx/index.test.ts +64 -0
  95. package/src/nx/index.ts +119 -0
  96. package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
  97. package/src/release/__tests__/candidates.test.ts +217 -0
  98. package/src/release/__tests__/core-properties.test.ts +4 -4
  99. package/src/release/__tests__/core-scenarios.test.ts +15 -5
  100. package/src/release/__tests__/core.test.ts +2 -2
  101. package/src/release/__tests__/fixture-repo.test.ts +22 -13
  102. package/src/release/__tests__/github-release.test.ts +13 -8
  103. package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
  104. package/src/release/__tests__/npm-auth.test.ts +12 -21
  105. package/src/release/__tests__/orchestration.test.ts +51 -7
  106. package/src/release/__tests__/publish-plan.test.ts +9 -3
  107. package/src/release/__tests__/retag-unpublished.test.ts +1 -1
  108. package/src/release/__tests__/trust-publisher.test.ts +133 -0
  109. package/src/release/bootstrap-npm-packages.ts +88 -0
  110. package/src/release/candidates.ts +225 -0
  111. package/src/release/core.ts +1 -0
  112. package/src/release/github-release.ts +2 -2
  113. package/src/release/index.ts +346 -56
  114. package/src/release/npm-auth.ts +4 -33
  115. package/src/release/orchestration.ts +12 -2
  116. package/src/release/publish-plan.ts +1 -1
  117. package/managed/raw/.git-format-staged.yml +0 -47
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
7
7
  "smoo": "./bin/smoo"
8
8
  },
9
9
  "scripts": {
10
- "test": "bun test"
10
+ "test": "nx run cli:test"
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "module": "./dist/index.js",
@@ -53,6 +53,7 @@
53
53
  "@smoothbricks/validation": "0.1.1",
54
54
  "commander": "^14.0.3",
55
55
  "publint": "^0.3.18",
56
+ "semver": "^7.7.4",
56
57
  "sherif": "^1.11.1",
57
58
  "tslib": "^2.8.1"
58
59
  },
@@ -71,11 +72,22 @@
71
72
  "dependsOn": [
72
73
  "build"
73
74
  ]
75
+ },
76
+ "test": {
77
+ "executor": "nx:run-commands",
78
+ "dependsOn": [
79
+ "^build"
80
+ ],
81
+ "options": {
82
+ "command": "bun test",
83
+ "cwd": "{projectRoot}"
84
+ }
74
85
  }
75
86
  },
76
87
  "tags": [
77
88
  "npm:public"
78
- ]
89
+ ],
90
+ "name": "cli"
79
91
  },
80
92
  "license": "MIT",
81
93
  "publishConfig": {
package/src/cli.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command, CommanderError } from 'commander';
2
+ import { cliPackageVersion } from './lib/cli-package.js';
2
3
  import { findRepoRoot } from './lib/run.js';
3
4
 
4
5
  export async function runCli(argv = process.argv.slice(2)): Promise<void> {
@@ -20,7 +21,12 @@ export async function runCli(argv = process.argv.slice(2)): Promise<void> {
20
21
 
21
22
  function buildProgram(): Command {
22
23
  const program = new Command();
23
- program.name('smoo').description('SmoothBricks monorepo tooling').exitOverride().showHelpAfterError();
24
+ program
25
+ .name('smoo')
26
+ .description('SmoothBricks monorepo tooling')
27
+ .version(cliPackageVersion, '-v, --version', 'print smoo version')
28
+ .exitOverride()
29
+ .showHelpAfterError();
24
30
 
25
31
  const monorepo = program.command('monorepo').description('Manage SmoothBricks-style monorepos');
26
32
  monorepo
@@ -33,12 +39,21 @@ function buildProgram(): Command {
33
39
  });
34
40
  monorepo
35
41
  .command('validate')
42
+ .option('--fix', 'apply safe monorepo policy fixes before validation')
36
43
  .option('--fail-fast', 'stop after the first failing validation pack')
37
44
  .option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
38
- .action(async (options: { failFast?: boolean; onlyIfNewWorkspacePackage?: boolean }) => {
39
- const { validateMonorepo } = await import('./monorepo/index.js');
40
- await validateMonorepo(await findRepoRoot(), options);
41
- });
45
+ .option('--verbose', 'print validation progress and successful checks')
46
+ .action(
47
+ async (options: {
48
+ fix?: boolean;
49
+ failFast?: boolean;
50
+ onlyIfNewWorkspacePackage?: boolean;
51
+ verbose?: boolean;
52
+ }) => {
53
+ const { validateMonorepo } = await import('./monorepo/index.js');
54
+ await validateMonorepo(await findRepoRoot(), options);
55
+ },
56
+ );
42
57
  monorepo.command('update').action(async () => {
43
58
  const { updateManagedFiles } = await import('./monorepo/index.js');
44
59
  updateManagedFiles(await findRepoRoot());
@@ -51,10 +66,13 @@ function buildProgram(): Command {
51
66
  const { diffManagedFiles } = await import('./monorepo/index.js');
52
67
  diffManagedFiles(await findRepoRoot());
53
68
  });
54
- monorepo.command('validate-commit-msg <commitMsgFile>').action(async (commitMsgFile: string) => {
55
- const { validateCommitMessageFile } = await import('./monorepo/index.js');
56
- validateCommitMessageFile(commitMsgFile);
57
- });
69
+ monorepo
70
+ .command('validate-commit-msg <commitMsgFile>')
71
+ .option('--fix', 'format the commit message before validation')
72
+ .action(async (commitMsgFile: string, options: { fix?: boolean }) => {
73
+ const { validateCommitMessageFile } = await import('./monorepo/index.js');
74
+ validateCommitMessageFile(commitMsgFile, options, await findRepoRoot());
75
+ });
58
76
  monorepo.command('sync-bun-lockfile-versions').action(async () => {
59
77
  const { syncBunLockfileVersions } = await import('./monorepo/index.js');
60
78
  syncBunLockfileVersions(await findRepoRoot());
@@ -62,10 +80,10 @@ function buildProgram(): Command {
62
80
  monorepo
63
81
  .command('list-release-packages')
64
82
  .option('--fail-empty', 'fail when no owned release packages are found')
65
- .option('--github-output <path>', 'append projects=<packages> to a GitHub Actions output file')
83
+ .option('--github-output <path>', 'append projects=<nx-projects> to a GitHub Actions output file')
66
84
  .action(async (options: { failEmpty?: boolean; githubOutput?: string }) => {
67
- const { listReleasePackagesForNx } = await import('./monorepo/index.js');
68
- const packages = listReleasePackagesForNx(await findRepoRoot(), options);
85
+ const { listReleaseProjectNamesForNx } = await import('./monorepo/index.js');
86
+ const packages = listReleaseProjectNamesForNx(await findRepoRoot(), options);
69
87
  if (!options.githubOutput) {
70
88
  console.log(packages);
71
89
  }
@@ -74,6 +92,30 @@ function buildProgram(): Command {
74
92
  const { validatePublicPackageTags } = await import('./monorepo/index.js');
75
93
  validatePublicPackageTags(await findRepoRoot());
76
94
  });
95
+ monorepo
96
+ .command('setup-test-tracing')
97
+ .description('Configure LMAO Bun test tracing for workspace packages')
98
+ .option('--all', 'configure every workspace package')
99
+ .option('--projects <projects>', 'comma-separated Nx project names, package names, or package roots')
100
+ .option('--op-context-export <exportName>', 'named op context export imported by test-suite-tracer', 'opContext')
101
+ .option(
102
+ '--tracer-module <module>',
103
+ 'module specifier that exports defineTestTracer',
104
+ '@smoothbricks/lmao/testing/bun',
105
+ )
106
+ .option('--dry-run', 'print generator invocations without writing files')
107
+ .action(
108
+ async (options: {
109
+ all?: boolean;
110
+ projects?: string;
111
+ opContextExport?: string;
112
+ tracerModule?: string;
113
+ dryRun?: boolean;
114
+ }) => {
115
+ const { setupTestTracing } = await import('./monorepo/index.js');
116
+ await setupTestTracing(await findRepoRoot(), options);
117
+ },
118
+ );
77
119
  const release = program.command('release').description('Version, publish, and create GitHub Releases');
78
120
  release.command('npm-status').action(async () => {
79
121
  const { printReleaseState } = await import('./release/index.js');
@@ -91,7 +133,7 @@ function buildProgram(): Command {
91
133
  .command('version')
92
134
  .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
93
135
  .option('--dry-run [dryRun]', 'run without writing versions or tags')
94
- .option('--github-output <path>', 'append mode=<mode> and projects=<packages> to a GitHub Actions output file')
136
+ .option('--github-output <path>', 'append mode=<mode> and projects=<nx-projects> to a GitHub Actions output file')
95
137
  .action(async (options: { bump: string; dryRun?: string | boolean; githubOutput?: string }) => {
96
138
  const { releaseVersion } = await import('./release/index.js');
97
139
  await releaseVersion(await findRepoRoot(), {
@@ -142,19 +184,91 @@ function buildProgram(): Command {
142
184
  });
143
185
  },
144
186
  );
187
+ release
188
+ .command('bootstrap-npm-packages')
189
+ .alias('bootstrap')
190
+ .description('Publish minimal npm placeholder packages so trusted publishing can be configured')
191
+ .option('--dry-run [dryRun]', 'show placeholder publishes without logging in or publishing')
192
+ .option('--skip-login', 'skip npm browser login before publishing placeholders')
193
+ .option('--otp <otp>', 'npm one-time password for placeholder publish operations')
194
+ .option('--package <name...>', 'only bootstrap the selected owned release package names')
195
+ .action(async (options: { dryRun?: string | boolean; skipLogin?: boolean; otp?: string; package?: string[] }) => {
196
+ const { releaseBootstrapNpmPackages } = await import('./release/index.js');
197
+ await releaseBootstrapNpmPackages(await findRepoRoot(), {
198
+ dryRun: booleanOption(options.dryRun),
199
+ skipLogin: options.skipLogin === true,
200
+ otp: options.otp,
201
+ packages: options.package ?? [],
202
+ });
203
+ });
145
204
  release
146
205
  .command('trust-publisher')
147
206
  .description('Configure npm trusted publishing for owned release packages')
148
207
  .option('--dry-run [dryRun]', 'show npm trust changes without saving them')
208
+ .option('--bootstrap', 'publish missing npm placeholder packages before configuring trust')
149
209
  .option('--otp <otp>', 'npm one-time password for trust operations')
210
+ .option('--bootstrap-otp <otp>', 'npm one-time password for placeholder publishes during --bootstrap')
150
211
  .option('--skip-login', 'skip npm browser login before configuring trust')
151
- .action(async (options: { dryRun?: string | boolean; otp?: string; skipLogin?: boolean }) => {
152
- const { releaseTrustPublisher } = await import('./release/index.js');
153
- await releaseTrustPublisher(await findRepoRoot(), {
154
- dryRun: booleanOption(options.dryRun),
155
- otp: options.otp,
156
- skipLogin: options.skipLogin === true,
157
- });
212
+ .action(
213
+ async (options: {
214
+ dryRun?: string | boolean;
215
+ bootstrap?: boolean;
216
+ otp?: string;
217
+ bootstrapOtp?: string;
218
+ skipLogin?: boolean;
219
+ }) => {
220
+ const { releaseTrustPublisher } = await import('./release/index.js');
221
+ await releaseTrustPublisher(await findRepoRoot(), {
222
+ dryRun: booleanOption(options.dryRun),
223
+ bootstrap: options.bootstrap === true,
224
+ otp: options.otp,
225
+ bootstrapOtp: options.bootstrapOtp,
226
+ skipLogin: options.skipLogin === true,
227
+ });
228
+ },
229
+ );
230
+
231
+ const devenv = program.command('devenv').description('Manage the repository devenv shell');
232
+ devenv.command('update').action(async () => {
233
+ const { updateDevenv } = await import('./devenv/index.js');
234
+ await updateDevenv(await findRepoRoot());
235
+ });
236
+ devenv.command('reload').action(async () => {
237
+ const { reloadDevenv } = await import('./devenv/index.js');
238
+ await reloadDevenv(await findRepoRoot());
239
+ });
240
+
241
+ const nixpkgsOverlay = program.command('nixpkgs-overlay').description('Manage the repository nixpkgs overlay');
242
+ nixpkgsOverlay.command('update').action(async () => {
243
+ const { updateNixpkgsOverlay } = await import('./devenv/index.js');
244
+ await updateNixpkgsOverlay(await findRepoRoot());
245
+ });
246
+
247
+ const nx = program.command('nx').description('Nx workspace helpers');
248
+ nx.command('list-targets')
249
+ .description('List project:target entries for every Nx project')
250
+ .action(async () => {
251
+ const { listTargets } = await import('./nx/index.js');
252
+ await listTargets(await findRepoRoot());
253
+ });
254
+ nx.command('list-projects')
255
+ .description('List Nx projects matching filters')
256
+ .requiredOption('--with-target <target>', 'only include projects defining this target')
257
+ .action(async (options: { withTarget?: string }) => {
258
+ const { listProjects } = await import('./nx/index.js');
259
+ await listProjects(await findRepoRoot(), options);
260
+ });
261
+ nx.command('reset-cache')
262
+ .description('Run nx reset to clear Nx daemon and cache state')
263
+ .action(async () => {
264
+ const { resetCache } = await import('./nx/index.js');
265
+ await resetCache(await findRepoRoot());
266
+ });
267
+ nx.command('clean-cache')
268
+ .description('Remove local Nx cache directories when present')
269
+ .action(async () => {
270
+ const { cleanCache } = await import('./nx/index.js');
271
+ await cleanCache(await findRepoRoot());
158
272
  });
159
273
 
160
274
  const githubCi = program.command('github-ci').description('GitHub Actions helpers');
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import type { DevenvCommandShell } from './index.js';
3
+ import { reloadDevenv, updateDevenv, updateNixpkgsOverlay } from './index.js';
4
+
5
+ describe('devenv commands', () => {
6
+ it('runs devenv update in tooling/direnv', async () => {
7
+ const shell = new RecordingShell();
8
+
9
+ await updateDevenv('/repo', shell);
10
+
11
+ expect(shell.commands).toEqual([{ command: 'devenv', args: ['update'], cwd: '/repo/tooling/direnv' }]);
12
+ });
13
+
14
+ it('clears cached devenv directories before reloading direnv', async () => {
15
+ const shell = new RecordingShell();
16
+
17
+ await reloadDevenv('/repo', shell);
18
+
19
+ expect(shell.removals).toEqual(['/repo/tooling/direnv/.direnv', '/repo/tooling/direnv/.devenv']);
20
+ expect(shell.commands).toEqual([{ command: 'direnv', args: ['reload'], cwd: '/repo/tooling/direnv' }]);
21
+ });
22
+
23
+ it('runs nvfetcher through nix shell in the overlay directory', async () => {
24
+ const shell = new RecordingShell();
25
+
26
+ await updateNixpkgsOverlay('/repo', shell);
27
+
28
+ expect(shell.commands).toEqual([
29
+ {
30
+ command: 'nix',
31
+ args: ['shell', 'nixpkgs#nvfetcher', '-c', 'nvfetcher', '-o', '_sources'],
32
+ cwd: '/repo/tooling/direnv/nixpkgs-overlay',
33
+ },
34
+ ]);
35
+ });
36
+ });
37
+
38
+ class RecordingShell implements DevenvCommandShell {
39
+ readonly commands: { command: string; args: string[]; cwd: string }[] = [];
40
+ readonly removals: string[] = [];
41
+
42
+ async run(command: string, args: string[], cwd: string): Promise<void> {
43
+ this.commands.push({ command, args, cwd });
44
+ }
45
+
46
+ async remove(path: string): Promise<void> {
47
+ this.removals.push(path);
48
+ }
49
+ }
@@ -0,0 +1,38 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { run } from '../lib/run.js';
4
+
5
+ export interface DevenvCommandShell {
6
+ run(command: string, args: string[], cwd: string): Promise<void>;
7
+ remove(path: string): Promise<void>;
8
+ }
9
+
10
+ const defaultShell: DevenvCommandShell = {
11
+ run,
12
+ async remove(path) {
13
+ await rm(path, { recursive: true, force: true });
14
+ },
15
+ };
16
+
17
+ export async function updateDevenv(root: string, shell: DevenvCommandShell = defaultShell): Promise<void> {
18
+ await shell.run('devenv', ['update'], direnvRoot(root));
19
+ }
20
+
21
+ export async function reloadDevenv(root: string, shell: DevenvCommandShell = defaultShell): Promise<void> {
22
+ const cwd = direnvRoot(root);
23
+ await shell.remove(join(cwd, '.direnv'));
24
+ await shell.remove(join(cwd, '.devenv'));
25
+ await shell.run('direnv', ['reload'], cwd);
26
+ }
27
+
28
+ export async function updateNixpkgsOverlay(root: string, shell: DevenvCommandShell = defaultShell): Promise<void> {
29
+ await shell.run('nix', ['shell', 'nixpkgs#nvfetcher', '-c', 'nvfetcher', '-o', '_sources'], nixpkgsOverlayRoot(root));
30
+ }
31
+
32
+ export function direnvRoot(root: string): string {
33
+ return join(root, 'tooling', 'direnv');
34
+ }
35
+
36
+ export function nixpkgsOverlayRoot(root: string): string {
37
+ return join(direnvRoot(root), 'nixpkgs-overlay');
38
+ }
@@ -0,0 +1,18 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { readJsonObject, stringProperty } from './json.js';
3
+
4
+ export const smoothBricksCodebasePackageName = '@smoothbricks/codebase';
5
+ export const cliPackageVersion = readCliPackageVersion();
6
+
7
+ function readCliPackageVersion(): string {
8
+ const pkg = readJsonObject(fileURLToPath(new URL('../../package.json', import.meta.url)));
9
+ const version = pkg ? stringProperty(pkg, 'version') : null;
10
+ if (!version) {
11
+ throw new Error('Unable to read @smoothbricks/cli package version.');
12
+ }
13
+ return version;
14
+ }
15
+
16
+ export function isSmoothBricksCodebasePackageName(name: string | undefined): boolean {
17
+ return name === smoothBricksCodebasePackageName;
18
+ }
package/src/lib/run.ts CHANGED
@@ -26,6 +26,25 @@ export async function runStatus(
26
26
  return result.exitCode;
27
27
  }
28
28
 
29
+ export async function runResult(
30
+ command: string,
31
+ args: string[],
32
+ cwd: string,
33
+ env?: Record<string, string>,
34
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
35
+ const invocation = resolveCommandInvocation(cwd, command, args);
36
+ let shell = $`${invocation.command} ${invocation.args}`.cwd(cwd).nothrow().quiet();
37
+ if (env) {
38
+ shell = shell.env(mergeEnv(env));
39
+ }
40
+ const result = await shell;
41
+ return {
42
+ exitCode: result.exitCode,
43
+ stdout: decode(result.stdout),
44
+ stderr: decode(result.stderr),
45
+ };
46
+ }
47
+
29
48
  function mergeEnv(env: Record<string, string>): Record<string, string> {
30
49
  const merged: Record<string, string> = {};
31
50
  for (const [key, value] of Object.entries(process.env)) {
@@ -1,9 +1,11 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
2
  import { dirname, join, relative } from 'node:path';
3
+ import { isSmoothBricksCodebasePackageName } from './cli-package.js';
3
4
  import { hasOwn, hasOwnString, isRecord, readJson, readJsonObject, stringProperty } from './json.js';
4
5
 
5
6
  export interface PackageInfo {
6
7
  name: string;
8
+ projectName: string;
7
9
  version: string;
8
10
  private: boolean;
9
11
  tags: string[];
@@ -12,12 +14,28 @@ export interface PackageInfo {
12
14
  json: Record<string, unknown>;
13
15
  }
14
16
 
17
+ export interface PackageJson extends Record<string, unknown> {
18
+ name?: string;
19
+ version?: string;
20
+ private?: boolean;
21
+ workspaces?: unknown;
22
+ devDependencies?: unknown;
23
+ dependencies?: unknown;
24
+ nx?: unknown;
25
+ repository?: unknown;
26
+ }
27
+
15
28
  export interface RepositoryInfo {
16
29
  type: string;
17
30
  url: string;
18
31
  }
19
32
 
20
- export const workspaceDependencyFields = ['dependencies', 'devDependencies', 'optionalDependencies'] as const;
33
+ export const workspaceDependencyFields = [
34
+ 'dependencies',
35
+ 'devDependencies',
36
+ 'peerDependencies',
37
+ 'optionalDependencies',
38
+ ] as const;
21
39
 
22
40
  export function listPublicPackages(root: string): PackageInfo[] {
23
41
  return getWorkspacePackages(root).filter((pkg) => !pkg.private && pkg.tags.includes('npm:public'));
@@ -44,6 +62,15 @@ export function rootRepositoryInfo(root: string): RepositoryInfo | null {
44
62
  return rootPackage ? repositoryInfo(rootPackage) : null;
45
63
  }
46
64
 
65
+ export function rootPackageName(root: string): string | null {
66
+ const rootPackage = readPackageJsonObject(join(root, 'package.json'));
67
+ return rootPackage ? stringProperty(rootPackage, 'name') : null;
68
+ }
69
+
70
+ export function isSmoothBricksCodebase(root: string): boolean {
71
+ return isSmoothBricksCodebasePackageName(rootPackageName(root) ?? undefined);
72
+ }
73
+
47
74
  export function packageRepositoryInfo(pkg: PackageInfo): RepositoryInfo | null {
48
75
  return repositoryInfo(pkg.json);
49
76
  }
@@ -79,6 +106,7 @@ function getWorkspacePackagesForPatterns(root: string, workspacePatterns: string
79
106
  }
80
107
  packages.push({
81
108
  name: pkg.name,
109
+ projectName: pkg.projectName,
82
110
  version: pkg.version,
83
111
  private: pkg.private,
84
112
  tags: pkg.tags,
@@ -122,7 +150,7 @@ function getWorkspacePatternsFromPackageJson(pkg: Record<string, unknown>): stri
122
150
  }
123
151
 
124
152
  export function readPackageJson(path: string): PackageInfo | null {
125
- const parsed = readJsonObject(path);
153
+ const parsed = readPackageJsonObject(path);
126
154
  if (!isRecord(parsed) || !hasOwnString(parsed, 'name') || !hasOwnString(parsed, 'version')) {
127
155
  return null;
128
156
  }
@@ -130,6 +158,7 @@ export function readPackageJson(path: string): PackageInfo | null {
130
158
  const tags = getNxTags(parsed);
131
159
  return {
132
160
  name: parsed.name,
161
+ projectName: packageNxProjectName(parsed),
133
162
  version: parsed.version,
134
163
  private: privateValue,
135
164
  tags,
@@ -139,6 +168,16 @@ export function readPackageJson(path: string): PackageInfo | null {
139
168
  };
140
169
  }
141
170
 
171
+ function packageNxProjectName(pkg: Record<string, unknown>): string {
172
+ const nx = isRecord(pkg.nx) ? pkg.nx : null;
173
+ return (nx ? stringProperty(nx, 'name') : null) ?? stringProperty(pkg, 'name') ?? '';
174
+ }
175
+
176
+ export function readPackageJsonObject(path: string): PackageJson | null {
177
+ const parsed = readJsonObject(path);
178
+ return parsed;
179
+ }
180
+
142
181
  export function getNxTags(pkg: Record<string, unknown>): string[] {
143
182
  if (!hasOwn(pkg, 'nx') || !isRecord(pkg.nx) || !hasOwn(pkg.nx, 'tags') || !Array.isArray(pkg.nx.tags)) {
144
183
  return [];
@@ -20,6 +20,14 @@ describe('publish workflow definition', () => {
20
20
  );
21
21
  });
22
22
 
23
+ it('does not wire npm token secrets into repair or publish steps', () => {
24
+ const rendered = renderPublishWorkflowYaml({ repoName: '@smoothbricks/codebase' });
25
+
26
+ expect(rendered).not.toContain('NODE_AUTH_TOKEN');
27
+ expect(rendered).not.toContain('secrets.NPM_TOKEN');
28
+ expect(rendered).toContain('packages must already exist on npm and use trusted publishing/OIDC');
29
+ });
30
+
23
31
  it('bootstraps the self-hosted CLI before release commands only for the SmoothBricks repo', async () => {
24
32
  const smoothbricks = await publishWorkflowScenario({
25
33
  repoName: '@smoothbricks/codebase',
@@ -79,16 +87,16 @@ describe('publish workflow definition', () => {
79
87
  ],
80
88
  bump: 'patch',
81
89
  dryRun: false,
82
- version: { mode: 'new', projects: ['@scope/a', '@scope/b'] },
90
+ version: { mode: 'new', projects: ['a', 'b'] },
83
91
  });
84
92
 
85
93
  const outcome = await scenario.run();
86
94
 
87
95
  expect(outcome.validation).toEqual({
88
96
  checks: 1,
89
- builds: ['@scope/a', '@scope/b'],
90
- lints: ['@scope/a', '@scope/b'],
91
- tests: ['@scope/a', '@scope/b'],
97
+ builds: ['a', 'b'],
98
+ lints: ['a', 'b'],
99
+ tests: ['a', 'b'],
92
100
  validates: 1,
93
101
  });
94
102
  expect(outcome.publishSawValidatedRelease).toBe(true);
@@ -102,7 +110,7 @@ describe('publish workflow definition', () => {
102
110
  current: [{ tag: '@scope/a@1.2.0', npmMissing: true, githubMissing: true }],
103
111
  bump: 'prerelease',
104
112
  dryRun: true,
105
- version: { mode: 'new', projects: ['@scope/a'] },
113
+ version: { mode: 'new', projects: ['a'] },
106
114
  });
107
115
 
108
116
  const outcome = await scenario.run();
@@ -112,9 +120,9 @@ describe('publish workflow definition', () => {
112
120
  expect(outcome.repairBuildArtifacts).toEqual([]);
113
121
  expect(outcome.validation).toEqual({
114
122
  checks: 1,
115
- builds: ['@scope/a'],
116
- lints: ['@scope/a'],
117
- tests: ['@scope/a'],
123
+ builds: ['a'],
124
+ lints: ['a'],
125
+ tests: ['a'],
118
126
  validates: 1,
119
127
  });
120
128
  expect(outcome.publishRan).toBe(true);
@@ -164,7 +172,6 @@ function publishWorkflowScenario(config: WorkflowScenarioConfig): { run(): Promi
164
172
  const state = new WorkflowScenarioState(config);
165
173
  await runPublishWorkflow(definePublishWorkflow({ repoName: config.repoName }), {
166
174
  inputs: { bump: config.bump, dryRun: config.dryRun },
167
- nodeAuthToken: true,
168
175
  callbacks: state.callbacks(),
169
176
  });
170
177
  return state.outcome();
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { formatCommitMessage, validateCommitMessage } from './commit-msg.js';
3
+
4
+ describe('commit message validation', () => {
5
+ it('accepts Nx conventional commit types and configured scopes', () => {
6
+ expect(validateCommitMessage('types(cli): expose public API\n', { validScopes: new Set(['cli']) })).toBeNull();
7
+ });
8
+
9
+ it('rejects scopes outside configured Nx names', () => {
10
+ expect(
11
+ validateCommitMessage('fix(@smoothbricks/cli): repair release\n', { validScopes: new Set(['cli']) }),
12
+ ).toContain('Invalid conventional commit scope');
13
+ });
14
+ });
15
+
16
+ describe('commit message formatting', () => {
17
+ it('wraps prose and preserves markdown blocks', () => {
18
+ const message = formatCommitMessage(
19
+ [
20
+ 'fix(cli): wrap commit bodies ',
21
+ '',
22
+ 'This paragraph is intentionally long enough to be wrapped by the injected formatter callback while preserving non-prose markdown sections.',
23
+ '',
24
+ '```',
25
+ 'long log errors should stay exactly as they are because this is a fenced block',
26
+ '```',
27
+ '',
28
+ '> quoted markdown should also stay as a single untouched line even when it is long enough to otherwise need wrapping',
29
+ ].join('\n'),
30
+ { wrapBody: (paragraph) => paragraph.replace(' while ', '\nwhile ') },
31
+ );
32
+
33
+ expect(message).toBe(`fix(cli): wrap commit bodies
34
+
35
+ This paragraph is intentionally long enough to be wrapped by the injected formatter callback
36
+ while preserving non-prose markdown sections.
37
+
38
+ \`\`\`
39
+ long log errors should stay exactly as they are because this is a fenced block
40
+ \`\`\`
41
+
42
+ > quoted markdown should also stay as a single untouched line even when it is long enough to otherwise need wrapping
43
+ `);
44
+ });
45
+ });