@smoothbricks/cli 0.2.0 → 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 (64) hide show
  1. package/README.md +27 -23
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +19 -3
  4. package/dist/generate/index.d.ts +20 -0
  5. package/dist/generate/index.d.ts.map +1 -0
  6. package/dist/generate/index.js +47 -0
  7. package/dist/lib/run.d.ts +1 -0
  8. package/dist/lib/run.d.ts.map +1 -1
  9. package/dist/lib/run.js +19 -0
  10. package/dist/lib/workspace.d.ts +10 -0
  11. package/dist/lib/workspace.d.ts.map +1 -1
  12. package/dist/lib/workspace.js +55 -16
  13. package/dist/monorepo/lockfile.d.ts.map +1 -1
  14. package/dist/monorepo/lockfile.js +9 -0
  15. package/dist/monorepo/package-policy.d.ts +6 -17
  16. package/dist/monorepo/package-policy.d.ts.map +1 -1
  17. package/dist/monorepo/package-policy.js +63 -1127
  18. package/dist/monorepo/publish-workflow.js +2 -3
  19. package/dist/nx/index.d.ts +1 -0
  20. package/dist/nx/index.d.ts.map +1 -1
  21. package/dist/nx/index.js +23 -5
  22. package/dist/release/candidates.js +1 -1
  23. package/dist/release/core.d.ts +3 -1
  24. package/dist/release/core.d.ts.map +1 -1
  25. package/dist/release/core.js +76 -9
  26. package/dist/release/github-release.d.ts +1 -0
  27. package/dist/release/github-release.d.ts.map +1 -1
  28. package/dist/release/github-release.js +1 -1
  29. package/dist/release/index.d.ts +9 -3
  30. package/dist/release/index.d.ts.map +1 -1
  31. package/dist/release/index.js +265 -39
  32. package/dist/release/orchestration.d.ts +10 -2
  33. package/dist/release/orchestration.d.ts.map +1 -1
  34. package/dist/release/orchestration.js +33 -3
  35. package/managed/templates/github/workflows/ci.yml +3 -2
  36. package/package.json +7 -12
  37. package/src/cli.ts +21 -4
  38. package/src/generate/index.ts +92 -0
  39. package/src/lib/run.ts +25 -0
  40. package/src/lib/workspace.ts +72 -16
  41. package/src/monorepo/lockfile.ts +9 -0
  42. package/src/monorepo/package-policy.test.ts +250 -22
  43. package/src/monorepo/package-policy.ts +88 -1351
  44. package/src/monorepo/publish-workflow.ts +2 -3
  45. package/src/nx/index.test.ts +9 -0
  46. package/src/nx/index.ts +25 -5
  47. package/src/release/__tests__/candidates.test.ts +53 -8
  48. package/src/release/__tests__/core-properties.test.ts +2 -2
  49. package/src/release/__tests__/core-scenarios.test.ts +12 -15
  50. package/src/release/__tests__/core.test.ts +50 -3
  51. package/src/release/__tests__/fixture-repo.test.ts +68 -18
  52. package/src/release/__tests__/github-release.test.ts +9 -16
  53. package/src/release/__tests__/orchestration.test.ts +91 -5
  54. package/src/release/__tests__/retag-unpublished.test.ts +9 -9
  55. package/src/release/__tests__/trust-publisher.test.ts +65 -33
  56. package/src/release/candidates.ts +1 -1
  57. package/src/release/core.ts +95 -10
  58. package/src/release/github-release.ts +1 -1
  59. package/src/release/index.ts +343 -49
  60. package/src/release/orchestration.ts +50 -5
  61. package/dist/nx-version-actions.cjs +0 -25
  62. package/dist/nx-version-actions.d.cts +0 -6
  63. package/dist/nx-version-actions.d.cts.map +0 -1
  64. package/src/nx-version-actions.cts +0 -36
@@ -17,6 +17,7 @@ defaults:
17
17
 
18
18
  jobs:
19
19
  main:
20
+ name: Validate
20
21
  runs-on: ubuntu-latest
21
22
  timeout-minutes: 45
22
23
  env:
@@ -81,8 +82,8 @@ jobs:
81
82
 
82
83
  # Step 10
83
84
  - name: 📎 Upload trace DBs
84
- if: ${{ failure() }}
85
- uses: actions/upload-artifact@v4
85
+ if: ${{ always() }}
86
+ uses: actions/upload-artifact@v7.0.1
86
87
  with:
87
88
  name: trace-results-${{ github.run_id }}
88
89
  path: packages/*/.trace-results.db
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": "nx run cli:test"
10
+ "test": "nx run cli:test --tui=false --outputStyle=stream"
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "module": "./dist/index.js",
@@ -33,11 +33,6 @@
33
33
  "development": "./src/monorepo/git-config.ts",
34
34
  "import": "./dist/monorepo/git-config.js",
35
35
  "default": "./dist/monorepo/git-config.js"
36
- },
37
- "./nx-version-actions": {
38
- "types": "./dist/nx-version-actions.d.cts",
39
- "require": "./dist/nx-version-actions.cjs",
40
- "default": "./dist/nx-version-actions.cjs"
41
36
  }
42
37
  },
43
38
  "files": [
@@ -50,6 +45,7 @@
50
45
  ],
51
46
  "dependencies": {
52
47
  "@arethetypeswrong/cli": "^0.18.2",
48
+ "@smoothbricks/nx-plugin": "0.2.0",
53
49
  "@smoothbricks/validation": "0.1.1",
54
50
  "commander": "^14.0.3",
55
51
  "publint": "^0.3.18",
@@ -74,13 +70,12 @@
74
70
  ]
75
71
  },
76
72
  "test": {
77
- "executor": "nx:run-commands",
78
- "dependsOn": [
79
- "^build"
80
- ],
73
+ "executor": "@smoothbricks/nx-plugin:bounded-exec",
81
74
  "options": {
82
75
  "command": "bun test",
83
- "cwd": "{projectRoot}"
76
+ "cwd": "{projectRoot}",
77
+ "timeoutMs": 120000,
78
+ "killAfterMs": 1e4
84
79
  }
85
80
  }
86
81
  },
package/src/cli.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command, CommanderError } from 'commander';
2
+ import { variants } from './generate/index.js';
2
3
  import { cliPackageVersion } from './lib/cli-package.js';
3
4
  import { findRepoRoot } from './lib/run.js';
4
5
 
@@ -116,6 +117,22 @@ function buildProgram(): Command {
116
117
  await setupTestTracing(await findRepoRoot(), options);
117
118
  },
118
119
  );
120
+ // `smoo g` / `smoo generate` — subcommands are driven by the variant
121
+ // registry in src/generate/index.ts. To add a new variant, add an entry
122
+ // there; the CLI wiring below picks it up automatically.
123
+ const g = program.command('g').alias('generate').description('Scaffold workspace packages and components');
124
+ for (const [variantName, variant] of Object.entries(variants)) {
125
+ const sub = g.command(`${variantName} <name>`).description(variant.description);
126
+ for (const opt of variant.options ?? []) {
127
+ sub.option(opt.flag, opt.description);
128
+ }
129
+ sub.option('--dry-run', 'preview changes without writing');
130
+ sub.action(async (name: string, options: Record<string, unknown>) => {
131
+ const { generate } = await import('./generate/index.js');
132
+ await generate(await findRepoRoot(), variantName, name, options);
133
+ });
134
+ }
135
+
119
136
  const release = program.command('release').description('Version, publish, and create GitHub Releases');
120
137
  release.command('npm-status').action(async () => {
121
138
  const { printReleaseState } = await import('./release/index.js');
@@ -206,24 +223,24 @@ function buildProgram(): Command {
206
223
  .description('Configure npm trusted publishing for owned release packages')
207
224
  .option('--dry-run [dryRun]', 'show npm trust changes without saving them')
208
225
  .option('--bootstrap', 'publish missing npm placeholder packages before configuring trust')
209
- .option('--otp <otp>', 'npm one-time password for trust operations')
210
226
  .option('--bootstrap-otp <otp>', 'npm one-time password for placeholder publishes during --bootstrap')
211
- .option('--skip-login', 'skip npm browser login before configuring trust')
227
+ .option('--skip-login', 'skip npm browser login before publishing placeholders during --bootstrap')
228
+ .option('--package <name...>', 'only configure the selected owned release package names')
212
229
  .action(
213
230
  async (options: {
214
231
  dryRun?: string | boolean;
215
232
  bootstrap?: boolean;
216
- otp?: string;
217
233
  bootstrapOtp?: string;
218
234
  skipLogin?: boolean;
235
+ package?: string[];
219
236
  }) => {
220
237
  const { releaseTrustPublisher } = await import('./release/index.js');
221
238
  await releaseTrustPublisher(await findRepoRoot(), {
222
239
  dryRun: booleanOption(options.dryRun),
223
240
  bootstrap: options.bootstrap === true,
224
- otp: options.otp,
225
241
  bootstrapOtp: options.bootstrapOtp,
226
242
  skipLogin: options.skipLogin === true,
243
+ packages: options.package ?? [],
227
244
  });
228
245
  },
229
246
  );
@@ -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
+ }
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,30 @@ 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
+
29
54
  export async function runResult(
30
55
  command: string,
31
56
  args: string[],
@@ -14,6 +14,15 @@ export interface PackageInfo {
14
14
  json: Record<string, unknown>;
15
15
  }
16
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
+
17
26
  export interface PackageJson extends Record<string, unknown> {
18
27
  name?: string;
19
28
  version?: string;
@@ -88,35 +97,66 @@ export function getWorkspacePackages(root: string): PackageInfo[] {
88
97
  return getWorkspacePackagesForPatterns(root, getWorkspacePatternsFromPackageJson(rootPackage.json));
89
98
  }
90
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
+
91
108
  function getWorkspacePackagesForPatterns(root: string, workspacePatterns: string[]): PackageInfo[] {
92
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[] = [];
93
145
  for (const pattern of workspacePatterns) {
94
146
  if (!pattern.endsWith('/*')) {
147
+ paths.push(join(root, pattern, 'package.json'));
95
148
  continue;
96
149
  }
150
+
97
151
  const parent = join(root, pattern.slice(0, -2));
98
152
  if (!statSync(parent, { throwIfNoEntry: false })?.isDirectory()) {
99
153
  continue;
100
154
  }
101
155
  for (const entry of readdirSync(parent)) {
102
- const pkgPath = join(parent, entry, 'package.json');
103
- const pkg = readPackageJson(pkgPath);
104
- if (!pkg?.name || !pkg.version) {
105
- continue;
106
- }
107
- packages.push({
108
- name: pkg.name,
109
- projectName: pkg.projectName,
110
- version: pkg.version,
111
- private: pkg.private,
112
- tags: pkg.tags,
113
- path: relative(root, dirname(pkgPath)),
114
- packageJsonPath: pkg.packageJsonPath,
115
- json: pkg.json,
116
- });
156
+ paths.push(join(parent, entry, 'package.json'));
117
157
  }
118
158
  }
119
- return packages.sort((a, b) => a.name.localeCompare(b.name));
159
+ return paths;
120
160
  }
121
161
 
122
162
  export function listPackageJsonRecords(root: string): PackageInfo[] {
@@ -168,6 +208,22 @@ export function readPackageJson(path: string): PackageInfo | null {
168
208
  };
169
209
  }
170
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
+
171
227
  function packageNxProjectName(pkg: Record<string, unknown>): string {
172
228
  const nx = isRecord(pkg.nx) ? pkg.nx : null;
173
229
  return (nx ? stringProperty(nx, 'name') : null) ?? stringProperty(pkg, 'name') ?? '';
@@ -6,6 +6,15 @@ export interface SyncBunLockfileVersionsOptions {
6
6
  log?: boolean;
7
7
  }
8
8
 
9
+ // Temporary Bun workaround. Delete this sync function, validateBunLockfileVersions,
10
+ // the `smoo monorepo sync-bun-lockfile-versions` command, and the matching Nx
11
+ // versionActions hook once supported Bun versions stop leaving workspace package
12
+ // versions stale in bun.lock after manifest bumps. Until then, `bun pm pack`
13
+ // rewrites `workspace:*` dependencies using those stale lockfile versions instead
14
+ // of the current package.json versions. Track removal against:
15
+ // - https://github.com/oven-sh/bun/issues/18906
16
+ // - https://github.com/oven-sh/bun/issues/20477
17
+ // - https://github.com/oven-sh/bun/issues/20829
9
18
  export function syncBunLockfileVersions(root: string, options: SyncBunLockfileVersionsOptions = {}): number {
10
19
  const log = options.log ?? true;
11
20
  const lockfilePath = join(root, 'bun.lock');