@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.
- package/README.md +155 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -7
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- package/dist/lib/run.d.ts +5 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +13 -0
- package/dist/lib/workspace.d.ts +15 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +25 -2
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +19 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +104 -16
- package/dist/monorepo/managed-files.js +1 -1
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +22 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1167 -8
- package/dist/monorepo/packed-package.d.ts +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +68 -36
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +238 -19
- package/dist/monorepo/publish-workflow.d.ts +0 -4
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +5 -12
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +24 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +94 -0
- package/dist/release/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +1 -0
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/github-release.js +2 -2
- package/dist/release/index.d.ts +25 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +247 -49
- package/dist/release/npm-auth.d.ts +0 -1
- package/dist/release/npm-auth.d.ts.map +1 -1
- package/dist/release/npm-auth.js +4 -16
- package/dist/release/orchestration.d.ts +2 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +9 -1
- package/dist/release/publish-plan.js +1 -1
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +1 -0
- package/package.json +15 -3
- package/src/cli.ts +134 -20
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +41 -2
- package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +142 -16
- package/src/monorepo/managed-files.ts +1 -1
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +902 -0
- package/src/monorepo/package-policy.ts +1393 -7
- package/src/monorepo/packed-package.ts +89 -37
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +311 -20
- package/src/monorepo/publish-workflow.ts +8 -16
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +64 -0
- package/src/nx/index.ts +119 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +217 -0
- package/src/release/__tests__/core-properties.test.ts +4 -4
- package/src/release/__tests__/core-scenarios.test.ts +15 -5
- package/src/release/__tests__/core.test.ts +2 -2
- package/src/release/__tests__/fixture-repo.test.ts +22 -13
- package/src/release/__tests__/github-release.test.ts +13 -8
- package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
- package/src/release/__tests__/npm-auth.test.ts +12 -21
- package/src/release/__tests__/orchestration.test.ts +51 -7
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +1 -1
- package/src/release/__tests__/trust-publisher.test.ts +133 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +1 -0
- package/src/release/github-release.ts +2 -2
- package/src/release/index.ts +346 -56
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +12 -2
- package/src/release/publish-plan.ts +1 -1
- 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.
|
|
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": "
|
|
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
|
|
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
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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=<
|
|
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 {
|
|
68
|
-
const packages =
|
|
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=<
|
|
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(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
otp
|
|
156
|
-
|
|
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)) {
|
package/src/lib/workspace.ts
CHANGED
|
@@ -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 = [
|
|
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 =
|
|
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: ['
|
|
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: ['
|
|
90
|
-
lints: ['
|
|
91
|
-
tests: ['
|
|
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: ['
|
|
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: ['
|
|
116
|
-
lints: ['
|
|
117
|
-
tests: ['
|
|
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
|
+
});
|