@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
@@ -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,92 @@
1
+ import { run } from '../lib/run.js';
2
+
3
+ const plugin = '@smoothbricks/nx-plugin';
4
+
5
+ // Variant registry — drives both CLI subcommands and dispatch.
6
+ //
7
+ // Each entry maps a `smoo g <variant>` subcommand to an Nx generator in
8
+ // @smoothbricks/nx-plugin. cli.ts iterates this record to register
9
+ // commander subcommands, so adding a variant here wires it end-to-end.
10
+ //
11
+ // Generator implementations:
12
+ // create-package packages/nx-plugin/src/generators/create-package/generator.ts
13
+ // make-public packages/nx-plugin/src/generators/make-public/generator.ts
14
+ //
15
+ // To add a new variant:
16
+ // 1. Add/extend a generator in packages/nx-plugin/src/generators/
17
+ // 2. Register it in packages/nx-plugin/generators.json
18
+ // 3. Add a variants entry below
19
+
20
+ export interface GenerateVariant {
21
+ /** Nx generator name inside @smoothbricks/nx-plugin (e.g. 'create-package'). */
22
+ generator: string;
23
+ /** One-line description shown in `smoo g --help`. */
24
+ description: string;
25
+ /** Build the generator-specific args from the positional <name>. */
26
+ args: (name: string) => string[];
27
+ /** Extra CLI flags beyond the universal --dry-run. */
28
+ options?: readonly VariantOption[];
29
+ }
30
+
31
+ interface VariantOption {
32
+ /** Commander flag syntax (e.g. '--public'). */
33
+ flag: string;
34
+ /** Help text for this flag. */
35
+ description: string;
36
+ }
37
+
38
+ export const variants: Record<string, GenerateVariant> = {
39
+ 'ts-lib': {
40
+ generator: 'create-package',
41
+ description: 'Create a TypeScript library package',
42
+ args: (name) => ['--name', name, '--variant', 'ts-lib'],
43
+ options: [{ flag: '--public', description: 'configure for npm publication' }],
44
+ },
45
+ 'ts-zig': {
46
+ generator: 'create-package',
47
+ description: 'Create a TypeScript + Zig/WASM hybrid package',
48
+ args: (name) => ['--name', name, '--variant', 'ts-zig'],
49
+ options: [{ flag: '--public', description: 'configure for npm publication' }],
50
+ },
51
+ 'make-public': {
52
+ generator: 'make-public',
53
+ description: 'Promote a private package to npm publication',
54
+ args: (name) => ['--project', name],
55
+ },
56
+ };
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Dispatch
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export async function generate(
63
+ root: string,
64
+ variantName: string,
65
+ name: string,
66
+ options: Record<string, unknown>,
67
+ ): Promise<void> {
68
+ const variant = variants[variantName];
69
+ if (!variant) {
70
+ const known = Object.keys(variants).join(', ');
71
+ throw new Error(`Unknown generator variant "${variantName}". Available: ${known}`);
72
+ }
73
+
74
+ const args = ['g', `${plugin}:${variant.generator}`, ...variant.args(name)];
75
+ for (const opt of variant.options ?? []) {
76
+ const key = opt.flag.replace(/^--/, '');
77
+ if (options[key]) {
78
+ args.push(opt.flag);
79
+ }
80
+ }
81
+ if (options.dryRun) {
82
+ args.push('--dry-run');
83
+ }
84
+
85
+ await run('nx', args, root);
86
+
87
+ // Sync TypeScript project references so the new package is immediately
88
+ // visible in the build graph without a manual `nx sync` step.
89
+ if (!options.dryRun) {
90
+ await run('nx', ['sync'], root);
91
+ }
92
+ }
@@ -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
@@ -1,3 +1,4 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { dirname, join } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
@@ -26,6 +27,49 @@ export async function runStatus(
26
27
  return result.exitCode;
27
28
  }
28
29
 
30
+ export async function runInteractiveStatus(
31
+ command: string,
32
+ args: string[],
33
+ cwd: string,
34
+ env?: Record<string, string>,
35
+ ): Promise<number> {
36
+ const invocation = resolveCommandInvocation(cwd, command, args);
37
+ return new Promise((resolve, reject) => {
38
+ const child = spawn(invocation.command, invocation.args, {
39
+ cwd,
40
+ env: env ? mergeEnv(env) : process.env,
41
+ stdio: 'inherit',
42
+ });
43
+ child.on('error', reject);
44
+ child.on('close', (code, signal) => {
45
+ if (signal) {
46
+ reject(new Error(`${command} ${args.join(' ')} terminated by signal ${signal}`));
47
+ return;
48
+ }
49
+ resolve(code ?? 1);
50
+ });
51
+ });
52
+ }
53
+
54
+ export async function runResult(
55
+ command: string,
56
+ args: string[],
57
+ cwd: string,
58
+ env?: Record<string, string>,
59
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
60
+ const invocation = resolveCommandInvocation(cwd, command, args);
61
+ let shell = $`${invocation.command} ${invocation.args}`.cwd(cwd).nothrow().quiet();
62
+ if (env) {
63
+ shell = shell.env(mergeEnv(env));
64
+ }
65
+ const result = await shell;
66
+ return {
67
+ exitCode: result.exitCode,
68
+ stdout: decode(result.stdout),
69
+ stderr: decode(result.stderr),
70
+ };
71
+ }
72
+
29
73
  function mergeEnv(env: Record<string, string>): Record<string, string> {
30
74
  const merged: Record<string, string> = {};
31
75
  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,37 @@ export interface PackageInfo {
12
14
  json: Record<string, unknown>;
13
15
  }
14
16
 
17
+ export interface WorkspacePackageManifest {
18
+ name: string;
19
+ projectName: string;
20
+ private: boolean;
21
+ path: string;
22
+ packageJsonPath: string;
23
+ json: Record<string, unknown>;
24
+ }
25
+
26
+ export interface PackageJson extends Record<string, unknown> {
27
+ name?: string;
28
+ version?: string;
29
+ private?: boolean;
30
+ workspaces?: unknown;
31
+ devDependencies?: unknown;
32
+ dependencies?: unknown;
33
+ nx?: unknown;
34
+ repository?: unknown;
35
+ }
36
+
15
37
  export interface RepositoryInfo {
16
38
  type: string;
17
39
  url: string;
18
40
  }
19
41
 
20
- export const workspaceDependencyFields = ['dependencies', 'devDependencies', 'optionalDependencies'] as const;
42
+ export const workspaceDependencyFields = [
43
+ 'dependencies',
44
+ 'devDependencies',
45
+ 'peerDependencies',
46
+ 'optionalDependencies',
47
+ ] as const;
21
48
 
22
49
  export function listPublicPackages(root: string): PackageInfo[] {
23
50
  return getWorkspacePackages(root).filter((pkg) => !pkg.private && pkg.tags.includes('npm:public'));
@@ -44,6 +71,15 @@ export function rootRepositoryInfo(root: string): RepositoryInfo | null {
44
71
  return rootPackage ? repositoryInfo(rootPackage) : null;
45
72
  }
46
73
 
74
+ export function rootPackageName(root: string): string | null {
75
+ const rootPackage = readPackageJsonObject(join(root, 'package.json'));
76
+ return rootPackage ? stringProperty(rootPackage, 'name') : null;
77
+ }
78
+
79
+ export function isSmoothBricksCodebase(root: string): boolean {
80
+ return isSmoothBricksCodebasePackageName(rootPackageName(root) ?? undefined);
81
+ }
82
+
47
83
  export function packageRepositoryInfo(pkg: PackageInfo): RepositoryInfo | null {
48
84
  return repositoryInfo(pkg.json);
49
85
  }
@@ -61,34 +97,66 @@ export function getWorkspacePackages(root: string): PackageInfo[] {
61
97
  return getWorkspacePackagesForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage.json));
62
98
  }
63
99
 
100
+ export function getWorkspacePackageManifests(root: string): WorkspacePackageManifest[] {
101
+ const rootPackage = readPackageJsonObject(join(root, 'package.json'));
102
+ if (!rootPackage) {
103
+ throw new Error('package.json not found or invalid');
104
+ }
105
+ return getWorkspacePackageManifestsForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage));
106
+ }
107
+
64
108
  function getWorkspacePackagesForPatterns(root: string, workspacePatterns: string[]): PackageInfo[] {
65
109
  const packages: PackageInfo[] = [];
110
+ for (const pkgPath of listWorkspacePackageJsonPaths(root, workspacePatterns)) {
111
+ const pkg = readPackageJson(pkgPath);
112
+ if (!pkg?.name || !pkg.version) {
113
+ continue;
114
+ }
115
+ packages.push({
116
+ name: pkg.name,
117
+ projectName: pkg.projectName,
118
+ version: pkg.version,
119
+ private: pkg.private,
120
+ tags: pkg.tags,
121
+ path: relative(root, dirname(pkgPath)),
122
+ packageJsonPath: pkg.packageJsonPath,
123
+ json: pkg.json,
124
+ });
125
+ }
126
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
127
+ }
128
+
129
+ function getWorkspacePackageManifestsForPatterns(
130
+ root: string,
131
+ workspacePatterns: string[],
132
+ ): WorkspacePackageManifest[] {
133
+ const manifests: WorkspacePackageManifest[] = [];
134
+ for (const pkgPath of listWorkspacePackageJsonPaths(root, workspacePatterns)) {
135
+ const pkg = readWorkspacePackageManifest(pkgPath);
136
+ if (pkg) {
137
+ manifests.push(pkg);
138
+ }
139
+ }
140
+ return manifests.sort((a, b) => a.name.localeCompare(b.name));
141
+ }
142
+
143
+ function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]): string[] {
144
+ const paths: string[] = [];
66
145
  for (const pattern of workspacePatterns) {
67
146
  if (!pattern.endsWith('/*')) {
147
+ paths.push(join(root, pattern, 'package.json'));
68
148
  continue;
69
149
  }
150
+
70
151
  const parent = join(root, pattern.slice(0, -2));
71
152
  if (!statSync(parent, { throwIfNoEntry: false })?.isDirectory()) {
72
153
  continue;
73
154
  }
74
155
  for (const entry of readdirSync(parent)) {
75
- const pkgPath = join(parent, entry, 'package.json');
76
- const pkg = readPackageJson(pkgPath);
77
- if (!pkg?.name || !pkg.version) {
78
- continue;
79
- }
80
- packages.push({
81
- name: pkg.name,
82
- version: pkg.version,
83
- private: pkg.private,
84
- tags: pkg.tags,
85
- path: relative(root, dirname(pkgPath)),
86
- packageJsonPath: pkg.packageJsonPath,
87
- json: pkg.json,
88
- });
156
+ paths.push(join(parent, entry, 'package.json'));
89
157
  }
90
158
  }
91
- return packages.sort((a, b) => a.name.localeCompare(b.name));
159
+ return paths;
92
160
  }
93
161
 
94
162
  export function listPackageJsonRecords(root: string): PackageInfo[] {
@@ -122,7 +190,7 @@ function getWorkspacePatternsFromPackageJson(pkg: Record<string, unknown>): stri
122
190
  }
123
191
 
124
192
  export function readPackageJson(path: string): PackageInfo | null {
125
- const parsed = readJsonObject(path);
193
+ const parsed = readPackageJsonObject(path);
126
194
  if (!isRecord(parsed) || !hasOwnString(parsed, 'name') || !hasOwnString(parsed, 'version')) {
127
195
  return null;
128
196
  }
@@ -130,6 +198,7 @@ export function readPackageJson(path: string): PackageInfo | null {
130
198
  const tags = getNxTags(parsed);
131
199
  return {
132
200
  name: parsed.name,
201
+ projectName: packageNxProjectName(parsed),
133
202
  version: parsed.version,
134
203
  private: privateValue,
135
204
  tags,
@@ -139,6 +208,32 @@ export function readPackageJson(path: string): PackageInfo | null {
139
208
  };
140
209
  }
141
210
 
211
+ export function readWorkspacePackageManifest(path: string): WorkspacePackageManifest | null {
212
+ const parsed = readPackageJsonObject(path);
213
+ if (!isRecord(parsed) || !hasOwnString(parsed, 'name')) {
214
+ return null;
215
+ }
216
+ const privateValue = hasOwn(parsed, 'private') && typeof parsed.private === 'boolean' ? parsed.private : false;
217
+ return {
218
+ name: parsed.name,
219
+ projectName: packageNxProjectName(parsed),
220
+ private: privateValue,
221
+ path: dirname(path),
222
+ packageJsonPath: path,
223
+ json: parsed,
224
+ };
225
+ }
226
+
227
+ function packageNxProjectName(pkg: Record<string, unknown>): string {
228
+ const nx = isRecord(pkg.nx) ? pkg.nx : null;
229
+ return (nx ? stringProperty(nx, 'name') : null) ?? stringProperty(pkg, 'name') ?? '';
230
+ }
231
+
232
+ export function readPackageJsonObject(path: string): PackageJson | null {
233
+ const parsed = readJsonObject(path);
234
+ return parsed;
235
+ }
236
+
142
237
  export function getNxTags(pkg: Record<string, unknown>): string[] {
143
238
  if (!hasOwn(pkg, 'nx') || !isRecord(pkg.nx) || !hasOwn(pkg.nx, 'tags') || !Array.isArray(pkg.nx.tags)) {
144
239
  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
+ });