@smoothbricks/cli 0.1.0 → 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 +338 -88
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +136 -22
- 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/devenv.d.ts +6 -0
- package/dist/lib/devenv.d.ts.map +1 -0
- package/dist/lib/devenv.js +82 -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 +20 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +87 -7
- 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 +24 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +136 -15
- package/dist/monorepo/lockfile.d.ts +5 -1
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +45 -5
- package/dist/monorepo/managed-files.d.ts +1 -1
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +28 -11
- 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 +25 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1289 -27
- package/dist/monorepo/packed-manifest.d.ts +4 -0
- package/dist/monorepo/packed-manifest.d.ts.map +1 -0
- package/dist/monorepo/packed-manifest.js +51 -0
- 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 +69 -31
- 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 +240 -18
- package/dist/monorepo/publish-workflow.d.ts +82 -0
- package/dist/monorepo/publish-workflow.d.ts.map +1 -0
- package/dist/monorepo/publish-workflow.js +336 -0
- 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/nx-version-actions.cjs +25 -0
- package/dist/nx-version-actions.d.cts +6 -0
- package/dist/nx-version-actions.d.cts.map +1 -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 +51 -0
- package/dist/release/core.d.ts.map +1 -0
- package/dist/release/core.js +97 -0
- package/dist/release/github-release.d.ts +46 -0
- package/dist/release/github-release.d.ts.map +1 -0
- package/dist/release/github-release.js +97 -0
- package/dist/release/index.d.ts +39 -5
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +763 -130
- package/dist/release/npm-auth.d.ts +16 -0
- package/dist/release/npm-auth.d.ts.map +1 -0
- package/dist/release/npm-auth.js +39 -0
- package/dist/release/orchestration.d.ts +49 -0
- package/dist/release/orchestration.d.ts.map +1 -0
- package/dist/release/orchestration.js +113 -0
- package/dist/release/publish-plan.d.ts +17 -0
- package/dist/release/publish-plan.d.ts.map +1 -0
- package/dist/release/publish-plan.js +15 -0
- package/dist/release/retag-unpublished.d.ts +34 -0
- package/dist/release/retag-unpublished.d.ts.map +1 -0
- package/dist/release/retag-unpublished.js +77 -0
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +3 -0
- package/package.json +37 -4
- package/src/cli.ts +193 -40
- 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/devenv.test.ts +28 -0
- package/src/lib/devenv.ts +89 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +117 -7
- package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +322 -0
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +181 -15
- package/src/monorepo/lockfile.ts +52 -7
- package/src/monorepo/managed-files.ts +43 -13
- 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 +1527 -25
- package/src/monorepo/packed-manifest.ts +67 -0
- package/src/monorepo/packed-package.ts +90 -31
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +314 -19
- package/src/monorepo/publish-workflow.ts +422 -0
- 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/nx-version-actions.cts +36 -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 +149 -0
- package/src/release/__tests__/core-scenarios.test.ts +187 -0
- package/src/release/__tests__/core.test.ts +73 -0
- package/src/release/__tests__/fixture-repo.test.ts +314 -0
- package/src/release/__tests__/github-release.test.ts +149 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +114 -0
- package/src/release/__tests__/npm-auth.test.ts +120 -0
- package/src/release/__tests__/orchestration.test.ts +246 -0
- package/src/release/__tests__/publish-plan.test.ts +67 -0
- package/src/release/__tests__/retag-unpublished.test.ts +160 -0
- 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 +171 -0
- package/src/release/github-release.ts +134 -0
- package/src/release/index.ts +974 -138
- package/src/release/npm-auth.ts +93 -0
- package/src/release/orchestration.ts +196 -0
- package/src/release/publish-plan.ts +37 -0
- package/src/release/retag-unpublished.ts +122 -0
- package/managed/raw/.git-format-staged.yml +0 -47
- package/managed/templates/github/workflows/publish.yml +0 -136
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,11 +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
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
.option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
|
|
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
|
+
);
|
|
41
57
|
monorepo.command('update').action(async () => {
|
|
42
58
|
const { updateManagedFiles } = await import('./monorepo/index.js');
|
|
43
59
|
updateManagedFiles(await findRepoRoot());
|
|
@@ -50,72 +66,209 @@ function buildProgram(): Command {
|
|
|
50
66
|
const { diffManagedFiles } = await import('./monorepo/index.js');
|
|
51
67
|
diffManagedFiles(await findRepoRoot());
|
|
52
68
|
});
|
|
53
|
-
monorepo
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
});
|
|
57
76
|
monorepo.command('sync-bun-lockfile-versions').action(async () => {
|
|
58
77
|
const { syncBunLockfileVersions } = await import('./monorepo/index.js');
|
|
59
78
|
syncBunLockfileVersions(await findRepoRoot());
|
|
60
79
|
});
|
|
61
|
-
monorepo
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
monorepo
|
|
81
|
+
.command('list-release-packages')
|
|
82
|
+
.option('--fail-empty', 'fail when no owned release packages are found')
|
|
83
|
+
.option('--github-output <path>', 'append projects=<nx-projects> to a GitHub Actions output file')
|
|
84
|
+
.action(async (options: { failEmpty?: boolean; githubOutput?: string }) => {
|
|
85
|
+
const { listReleaseProjectNamesForNx } = await import('./monorepo/index.js');
|
|
86
|
+
const packages = listReleaseProjectNamesForNx(await findRepoRoot(), options);
|
|
87
|
+
if (!options.githubOutput) {
|
|
88
|
+
console.log(packages);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
65
91
|
monorepo.command('validate-public-tags').action(async () => {
|
|
66
92
|
const { validatePublicPackageTags } = await import('./monorepo/index.js');
|
|
67
93
|
validatePublicPackageTags(await findRepoRoot());
|
|
68
94
|
});
|
|
69
|
-
monorepo
|
|
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
|
+
);
|
|
119
|
+
const release = program.command('release').description('Version, publish, and create GitHub Releases');
|
|
120
|
+
release.command('npm-status').action(async () => {
|
|
70
121
|
const { printReleaseState } = await import('./release/index.js');
|
|
71
122
|
await printReleaseState(await findRepoRoot());
|
|
72
123
|
});
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
release
|
|
125
|
+
.command('repair-pending')
|
|
126
|
+
.description('Repair incomplete older release commits before releasing the current HEAD')
|
|
127
|
+
.option('--dry-run [dryRun]', 'run without pushing, publishing, or writing GitHub Releases')
|
|
128
|
+
.action(async (options: { dryRun?: string | boolean }) => {
|
|
129
|
+
const { releaseRepairPending } = await import('./release/index.js');
|
|
130
|
+
await releaseRepairPending(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
|
|
131
|
+
});
|
|
75
132
|
release
|
|
76
133
|
.command('version')
|
|
77
134
|
.option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
|
|
78
|
-
.option('--dry-run [dryRun]', 'run without writing versions
|
|
79
|
-
.
|
|
135
|
+
.option('--dry-run [dryRun]', 'run without writing versions or tags')
|
|
136
|
+
.option('--github-output <path>', 'append mode=<mode> and projects=<nx-projects> to a GitHub Actions output file')
|
|
137
|
+
.action(async (options: { bump: string; dryRun?: string | boolean; githubOutput?: string }) => {
|
|
80
138
|
const { releaseVersion } = await import('./release/index.js');
|
|
81
|
-
await releaseVersion(await findRepoRoot(), {
|
|
139
|
+
await releaseVersion(await findRepoRoot(), {
|
|
140
|
+
bump: options.bump,
|
|
141
|
+
dryRun: booleanOption(options.dryRun),
|
|
142
|
+
githubOutput: options.githubOutput,
|
|
143
|
+
});
|
|
82
144
|
});
|
|
83
145
|
release
|
|
84
146
|
.command('publish')
|
|
85
147
|
.option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
|
|
86
|
-
.option('--
|
|
87
|
-
.
|
|
88
|
-
.option('--dry-run [dryRun]', 'run without publishing')
|
|
89
|
-
.action(async (options: { bump: string; tag?: string; npmTag?: string; dryRun?: string | boolean }) => {
|
|
148
|
+
.option('--dry-run [dryRun]', 'run without pushing, publishing, or writing GitHub Releases')
|
|
149
|
+
.action(async (options: { bump: string; dryRun?: string | boolean }) => {
|
|
90
150
|
const { releasePublish } = await import('./release/index.js');
|
|
91
151
|
await releasePublish(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
|
|
92
152
|
});
|
|
93
153
|
release
|
|
94
|
-
.command('
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.option('--
|
|
98
|
-
.option('--
|
|
99
|
-
.option('--
|
|
154
|
+
.command('retag-unpublished')
|
|
155
|
+
.description('Move unpublished owned release tags to a later commit without bumping versions')
|
|
156
|
+
.argument('<tag...>', 'owned release tags to move, for example @scope/pkg@1.2.3')
|
|
157
|
+
.option('--to <ref>', 'commit or ref to move tags to', 'HEAD')
|
|
158
|
+
.option('--push', 'push moved tags with force-with-lease')
|
|
159
|
+
.option('--dispatch', 'push moved tags and start publish.yml with bump=auto')
|
|
160
|
+
.option('--remote <remote>', 'git remote used for pushed tags')
|
|
161
|
+
.option('--branch <branch>', 'branch used for publish workflow dispatch')
|
|
162
|
+
.option('--dry-run [dryRun]', 'validate and print the retag operation without mutating refs')
|
|
100
163
|
.action(
|
|
101
|
-
async (
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
async (
|
|
165
|
+
tags: string[],
|
|
166
|
+
options: {
|
|
167
|
+
to?: string;
|
|
168
|
+
push?: boolean;
|
|
169
|
+
dispatch?: boolean;
|
|
170
|
+
remote?: string;
|
|
171
|
+
branch?: string;
|
|
172
|
+
dryRun?: string | boolean;
|
|
173
|
+
},
|
|
174
|
+
) => {
|
|
175
|
+
const { releaseRetagUnpublished } = await import('./release/index.js');
|
|
176
|
+
await releaseRetagUnpublished(await findRepoRoot(), {
|
|
177
|
+
tags,
|
|
178
|
+
to: options.to,
|
|
179
|
+
push: options.push === true,
|
|
180
|
+
dispatch: options.dispatch === true,
|
|
181
|
+
remote: options.remote,
|
|
182
|
+
branch: options.branch,
|
|
183
|
+
dryRun: booleanOption(options.dryRun),
|
|
184
|
+
});
|
|
104
185
|
},
|
|
105
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
|
+
});
|
|
106
204
|
release
|
|
107
205
|
.command('trust-publisher')
|
|
108
|
-
.description('Configure npm trusted publishing for
|
|
206
|
+
.description('Configure npm trusted publishing for owned release packages')
|
|
109
207
|
.option('--dry-run [dryRun]', 'show npm trust changes without saving them')
|
|
208
|
+
.option('--bootstrap', 'publish missing npm placeholder packages before configuring trust')
|
|
110
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')
|
|
111
211
|
.option('--skip-login', 'skip npm browser login before configuring trust')
|
|
112
|
-
.action(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
otp
|
|
117
|
-
|
|
118
|
-
|
|
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());
|
|
119
272
|
});
|
|
120
273
|
|
|
121
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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mergeDevenvEnv, parseNulEnv } from './devenv.js';
|
|
3
|
+
|
|
4
|
+
describe('devenv environment loader', () => {
|
|
5
|
+
it('parses NUL-separated env output without splitting values on newlines or equals signs', () => {
|
|
6
|
+
const bytes = new TextEncoder().encode('SIMPLE=value\0MULTILINE=line 1\nline 2\0TOKEN=a=b=c\0');
|
|
7
|
+
|
|
8
|
+
expect(parseNulEnv(bytes)).toEqual({
|
|
9
|
+
SIMPLE: 'value',
|
|
10
|
+
MULTILINE: 'line 1\nline 2',
|
|
11
|
+
TOKEN: 'a=b=c',
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('merges devenv output over the existing environment instead of replacing it', () => {
|
|
16
|
+
expect(
|
|
17
|
+
mergeDevenvEnv(
|
|
18
|
+
{ GH_TOKEN: 'existing-token', NODE_AUTH_TOKEN: 'npm-token', PATH: '/usr/bin' },
|
|
19
|
+
{ DEVENV_ROOT: '/repo/tooling/direnv', PATH: '/nix/bin' },
|
|
20
|
+
),
|
|
21
|
+
).toEqual({
|
|
22
|
+
DEVENV_ROOT: '/repo/tooling/direnv',
|
|
23
|
+
GH_TOKEN: 'existing-token',
|
|
24
|
+
NODE_AUTH_TOKEN: 'npm-token',
|
|
25
|
+
PATH: '/nix/bin',
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { $ } from 'bun';
|
|
5
|
+
import { decode } from './run.js';
|
|
6
|
+
|
|
7
|
+
type EnvSnapshot = Record<string, string>;
|
|
8
|
+
|
|
9
|
+
export async function withDevenvEnv<T>(root: string, runWithEnv: () => Promise<T>): Promise<T> {
|
|
10
|
+
const snapshot = snapshotProcessEnv();
|
|
11
|
+
const env = await loadDevenvEnv(root);
|
|
12
|
+
replaceProcessEnv(mergeDevenvEnv(snapshot, env));
|
|
13
|
+
try {
|
|
14
|
+
return await runWithEnv();
|
|
15
|
+
} finally {
|
|
16
|
+
restoreProcessEnv(snapshot);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function loadDevenvEnv(root: string): Promise<EnvSnapshot> {
|
|
21
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-devenv-env-'));
|
|
22
|
+
const envPath = join(tempDir, 'env');
|
|
23
|
+
try {
|
|
24
|
+
const result = await $`devenv shell -- bash -lc ${'env -0 > "$1"'} bash ${envPath}`
|
|
25
|
+
.cwd(join(root, 'tooling', 'direnv'))
|
|
26
|
+
.quiet()
|
|
27
|
+
.nothrow();
|
|
28
|
+
if (result.exitCode !== 0) {
|
|
29
|
+
throw new Error('devenv shell failed. Ensure devenv is installed and the tooling/direnv shell is valid.');
|
|
30
|
+
}
|
|
31
|
+
return parseNulEnv(await readFile(envPath));
|
|
32
|
+
} finally {
|
|
33
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseNulEnv(bytes: Uint8Array): EnvSnapshot {
|
|
38
|
+
const env: EnvSnapshot = {};
|
|
39
|
+
let entryStart = 0;
|
|
40
|
+
for (let index = 0; index <= bytes.length; index += 1) {
|
|
41
|
+
if (index !== bytes.length && bytes[index] !== 0) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (index === entryStart) {
|
|
45
|
+
entryStart = index + 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const entry = decode(bytes.subarray(entryStart, index));
|
|
49
|
+
const separator = entry.indexOf('=');
|
|
50
|
+
if (separator <= 0) {
|
|
51
|
+
throw new Error('devenv shell produced an invalid environment entry.');
|
|
52
|
+
}
|
|
53
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
54
|
+
entryStart = index + 1;
|
|
55
|
+
}
|
|
56
|
+
return env;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mergeDevenvEnv(baseEnv: EnvSnapshot, devenvEnv: EnvSnapshot): EnvSnapshot {
|
|
60
|
+
return { ...baseEnv, ...devenvEnv };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function snapshotProcessEnv(): EnvSnapshot {
|
|
64
|
+
const snapshot: EnvSnapshot = {};
|
|
65
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
66
|
+
if (value !== undefined) {
|
|
67
|
+
snapshot[key] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return snapshot;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function replaceProcessEnv(env: EnvSnapshot): void {
|
|
74
|
+
for (const key of Object.keys(process.env)) {
|
|
75
|
+
delete process.env[key];
|
|
76
|
+
}
|
|
77
|
+
for (const [key, value] of Object.entries(env)) {
|
|
78
|
+
process.env[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function restoreProcessEnv(snapshot: EnvSnapshot): void {
|
|
83
|
+
for (const key of Object.keys(process.env)) {
|
|
84
|
+
delete process.env[key];
|
|
85
|
+
}
|
|
86
|
+
for (const [key, value] of Object.entries(snapshot)) {
|
|
87
|
+
process.env[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
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)) {
|