@smoothbricks/cli 0.1.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 (86) hide show
  1. package/README.md +289 -0
  2. package/bin/smoo +4 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +141 -0
  6. package/dist/github-ci/index.d.ts +11 -0
  7. package/dist/github-ci/index.d.ts.map +1 -0
  8. package/dist/github-ci/index.js +174 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/lib/json.d.ts +13 -0
  13. package/dist/lib/json.d.ts.map +1 -0
  14. package/dist/lib/json.js +57 -0
  15. package/dist/lib/run.d.ts +5 -0
  16. package/dist/lib/run.d.ts.map +1 -0
  17. package/dist/lib/run.js +64 -0
  18. package/dist/lib/workspace.d.ts +23 -0
  19. package/dist/lib/workspace.d.ts.map +1 -0
  20. package/dist/lib/workspace.js +101 -0
  21. package/dist/monorepo/commit-msg.d.ts +2 -0
  22. package/dist/monorepo/commit-msg.d.ts.map +1 -0
  23. package/dist/monorepo/commit-msg.js +34 -0
  24. package/dist/monorepo/git-config.d.ts +2 -0
  25. package/dist/monorepo/git-config.d.ts.map +1 -0
  26. package/dist/monorepo/git-config.js +37 -0
  27. package/dist/monorepo/index.d.ts +19 -0
  28. package/dist/monorepo/index.d.ts.map +1 -0
  29. package/dist/monorepo/index.js +59 -0
  30. package/dist/monorepo/lockfile.d.ts +2 -0
  31. package/dist/monorepo/lockfile.d.ts.map +1 -0
  32. package/dist/monorepo/lockfile.js +34 -0
  33. package/dist/monorepo/managed-files.d.ts +8 -0
  34. package/dist/monorepo/managed-files.d.ts.map +1 -0
  35. package/dist/monorepo/managed-files.js +130 -0
  36. package/dist/monorepo/nx-sync.d.ts +2 -0
  37. package/dist/monorepo/nx-sync.d.ts.map +1 -0
  38. package/dist/monorepo/nx-sync.js +5 -0
  39. package/dist/monorepo/package-hygiene.d.ts +3 -0
  40. package/dist/monorepo/package-hygiene.d.ts.map +1 -0
  41. package/dist/monorepo/package-hygiene.js +12 -0
  42. package/dist/monorepo/package-policy.d.ts +8 -0
  43. package/dist/monorepo/package-policy.d.ts.map +1 -0
  44. package/dist/monorepo/package-policy.js +279 -0
  45. package/dist/monorepo/packed-package.d.ts +2 -0
  46. package/dist/monorepo/packed-package.d.ts.map +1 -0
  47. package/dist/monorepo/packed-package.js +92 -0
  48. package/dist/monorepo/packs/index.d.ts +10 -0
  49. package/dist/monorepo/packs/index.d.ts.map +1 -0
  50. package/dist/monorepo/packs/index.js +87 -0
  51. package/dist/monorepo/runtime.d.ts +2 -0
  52. package/dist/monorepo/runtime.d.ts.map +1 -0
  53. package/dist/monorepo/runtime.js +37 -0
  54. package/dist/release/index.d.ts +24 -0
  55. package/dist/release/index.d.ts.map +1 -0
  56. package/dist/release/index.js +325 -0
  57. package/managed/raw/.git-format-staged.yml +47 -0
  58. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +66 -0
  59. package/managed/raw/tooling/git-hooks/commit-msg.sh +9 -0
  60. package/managed/raw/tooling/git-hooks/pre-commit.sh +19 -0
  61. package/managed/templates/github/actions/cache-nix-devenv/action.yml +79 -0
  62. package/managed/templates/github/actions/cache-node-modules/action.yml +13 -0
  63. package/managed/templates/github/actions/cache-nx/action.yml +21 -0
  64. package/managed/templates/github/actions/save-nix-devenv/action.yml +39 -0
  65. package/managed/templates/github/actions/setup-devenv/action.yml +63 -0
  66. package/managed/templates/github/workflows/ci.yml +98 -0
  67. package/managed/templates/github/workflows/publish.yml +136 -0
  68. package/package.json +68 -0
  69. package/src/cli.ts +149 -0
  70. package/src/github-ci/index.ts +196 -0
  71. package/src/index.ts +1 -0
  72. package/src/lib/json.ts +68 -0
  73. package/src/lib/run.ts +76 -0
  74. package/src/lib/workspace.ts +125 -0
  75. package/src/monorepo/commit-msg.ts +35 -0
  76. package/src/monorepo/git-config.ts +43 -0
  77. package/src/monorepo/index.ts +78 -0
  78. package/src/monorepo/lockfile.ts +37 -0
  79. package/src/monorepo/managed-files.ts +153 -0
  80. package/src/monorepo/nx-sync.ts +6 -0
  81. package/src/monorepo/package-hygiene.ts +14 -0
  82. package/src/monorepo/package-policy.ts +307 -0
  83. package/src/monorepo/packed-package.ts +101 -0
  84. package/src/monorepo/packs/index.ts +115 -0
  85. package/src/monorepo/runtime.ts +41 -0
  86. package/src/release/index.ts +394 -0
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # @smoothbricks/cli
2
+
3
+ `@smoothbricks/cli` provides `smoo`, the SmoothBricks monorepo automation CLI. It is the control plane for shared CI,
4
+ release, Git hook, package metadata, and publish validation conventions across SmoothBricks-style repositories.
5
+
6
+ The tool is intentionally convention-over-configuration. SmoothBricks repos use Nx, Bun, Nix, devenv, and direnv, so
7
+ `smoo` assumes those pieces exist instead of adding another local config file. Repos should be made correct by running
8
+ the mutating initialization path, then kept correct by the read-only validation path.
9
+
10
+ ## Install
11
+
12
+ Add the CLI to the root workspace:
13
+
14
+ ```bash
15
+ bun add -d @smoothbricks/cli
16
+ ```
17
+
18
+ The package exposes a Bun-native executable:
19
+
20
+ ```bash
21
+ smoo --help
22
+ ```
23
+
24
+ SmoothBricks itself self-hosts before `dist` exists by using `tooling/smoo`, which imports `packages/cli/src/cli.ts`
25
+ directly. Published installs use the package binary in `bin/smoo`, which imports built JavaScript from `dist`.
26
+
27
+ ## Core Commands
28
+
29
+ ```bash
30
+ smoo monorepo init [--runtime-only] [--sync-runtime]
31
+ smoo monorepo validate
32
+ smoo monorepo update
33
+ smoo monorepo check
34
+ smoo monorepo diff
35
+ smoo monorepo validate-commit-msg <commit-msg-file>
36
+ smoo monorepo sync-bun-lockfile-versions
37
+ smoo monorepo list-public-projects
38
+ smoo monorepo validate-public-tags
39
+ smoo monorepo release-state
40
+
41
+ smoo release version --bump <auto|patch|minor|major|prerelease> [--dry-run]
42
+ smoo release publish --bump <auto|patch|minor|major|prerelease> [--tag <tag>] [--npm-tag <tag>] [--dry-run]
43
+ smoo release github-release --tags <tags> --bump <auto|patch|minor|major|prerelease> [--tag <tag>] [--npm-tag <tag>] [--dry-run]
44
+ smoo release trust-publisher [--dry-run] [--otp <code>] [--skip-login]
45
+
46
+ smoo github-ci cleanup-cache
47
+ smoo github-ci nx-smart --target <target> --name <check-name> --step <number>
48
+ smoo github-ci nx-run-many --targets <targets> [--projects <projects>]
49
+ ```
50
+
51
+ ## Initialization
52
+
53
+ `smoo monorepo init` is the fix-first command. It mutates the repository toward the SmoothBricks convention instead of
54
+ only reporting drift.
55
+
56
+ It currently:
57
+
58
+ - Updates managed CI, release, hook, and formatting files.
59
+ - Ensures the local `tooling/smoo` source shim is executable when present.
60
+ - Synchronizes root runtime versions inside devenv, or when `--sync-runtime` is passed.
61
+ - Applies publish metadata defaults to `npm:public` packages.
62
+ - Normalizes internal workspace dependency ranges to `workspace:*`.
63
+ - Runs `sherif --fix --select highest` for broad monorepo package hygiene.
64
+ - Normalizes conditional export ordering so `types` comes first and `default` comes last.
65
+ - Adds `src` to package `files` when development-only exports intentionally point at source files.
66
+
67
+ The workspace dependency rule is generic. `smoo` does not know about individual package names such as `eslint-stdout`.
68
+ For every root or workspace `package.json`, if a dependency name matches an actual package in the same workspace, `smoo`
69
+ rewrites that range to `workspace:*`.
70
+
71
+ `smoo monorepo init --runtime-only` only synchronizes root runtime versions. It is used from direnv setup so
72
+ `packageManager`, `engines.node`, and `@types/node` stay aligned with the active devenv shell without duplicating that
73
+ policy in the direnv script.
74
+
75
+ Bun types are also a root runtime policy. The root `@types/bun` version follows the exact `packageManager` Bun version,
76
+ while package manifests are not forced to depend on Bun just because a test tsconfig opts into Bun globals. If a package
77
+ does explicitly declare `@types/bun`, `sherif` can keep duplicate declarations consistent, but `smoo` owns the semantic
78
+ root `bun@x.y.z` to `@types/bun x.y.z` relationship.
79
+
80
+ ## Validation
81
+
82
+ `smoo monorepo validate` is the read-only gate. It should pass in local shells and CI after packages have been built.
83
+
84
+ It checks:
85
+
86
+ - Managed file drift.
87
+ - Root package policy.
88
+ - Root Bun type version matches the Bun package manager version.
89
+ - Nx release policy.
90
+ - Public package tag policy.
91
+ - Public package metadata.
92
+ - Workspace dependency ranges.
93
+ - `sherif` package hygiene.
94
+ - Packed public package artifacts with `publint`.
95
+ - Packed public package type resolution with the `attw` CLI.
96
+
97
+ The packed-package checks validate what npm users will install, not only the source tree. `smoo` packs each `npm:public`
98
+ package with Bun, runs `publint` on the tarball, then runs `attw` on the same tarball.
99
+
100
+ The `attw` check uses the `node16` profile and ignores the CJS-to-ESM warning. SmoothBricks packages are ESM-first, so
101
+ CommonJS consumers can use dynamic import. Node 10-only subpath failures are intentionally ignored because Node 10 is
102
+ not part of the supported package contract.
103
+
104
+ ## Publishable Packages
105
+
106
+ Publishability is declared with an Nx tag:
107
+
108
+ ```json
109
+ {
110
+ "nx": {
111
+ "tags": ["npm:public"]
112
+ }
113
+ }
114
+ ```
115
+
116
+ This tag is the source of truth for release selection, metadata validation, and publish artifact validation. `smoo`
117
+ never relies on a second hardcoded package list.
118
+
119
+ Rules:
120
+
121
+ - Publishable packages must not be private.
122
+ - Private packages must not have `npm:public`.
123
+ - Every non-private workspace package must have `npm:public`.
124
+ - Public packages must define license metadata.
125
+ - Public packages must publish with `publishConfig.access = "public"`.
126
+ - Public package repositories must point at the root repo and include `repository.directory`.
127
+ - Public packages must define `files`.
128
+ - Public library packages must define `types`.
129
+ - Public packages must define either `exports` or `bin`.
130
+
131
+ `smoo monorepo init` fixes the metadata that can be derived from the root package. For example, it copies the root
132
+ license and repository URL, then sets each package's `repository.directory` from its workspace path.
133
+
134
+ ## Managed Files
135
+
136
+ `smoo monorepo update` writes the managed files into a repository.
137
+
138
+ Managed files include:
139
+
140
+ - `.git-format-staged.yml`
141
+ - Git hook scripts under `tooling/git-hooks`
142
+ - direnv/GitHub Actions bootstrap scripts under `tooling/direnv`
143
+ - GitHub Actions workflows under `.github/workflows`
144
+ - Local composite GitHub Actions under `.github/actions`
145
+
146
+ When a managed target is a symlink, `smoo` leaves it alone. SmoothBricks uses symlinks back to `packages/cli/managed` so
147
+ changes to the CLI package are tested immediately. Downstream repos receive ordinary committed copies.
148
+
149
+ Use:
150
+
151
+ ```bash
152
+ smoo monorepo update
153
+ smoo monorepo check
154
+ smoo monorepo diff
155
+ ```
156
+
157
+ `check` fails when a managed file is missing or stale. `diff` reports drift without writing files.
158
+
159
+ ## Git Hooks
160
+
161
+ The generated pre-commit hook runs `git-format-staged` from the repository root with `tooling`, `node_modules/.bin`, and
162
+ the devenv profile on `PATH`.
163
+
164
+ The generated commit-msg hook delegates conventional commit validation to:
165
+
166
+ ```bash
167
+ smoo monorepo validate-commit-msg <commit-msg-file>
168
+ ```
169
+
170
+ This keeps hook behavior consistent with CI and avoids duplicating commit message parsing in shell.
171
+
172
+ ## GitHub Actions
173
+
174
+ The generated workflows keep readable YAML and named top-level steps, while larger logic lives in `smoo` commands,
175
+ post-checkout composite actions, or the small pre-smoo bootstrap script. Checkout stays inline in each workflow because
176
+ repository-local composite actions do not exist until `actions/checkout` has populated the working tree.
177
+
178
+ CI uses explicit lint, test, and build phases. The publish workflow does the same before running release commands so
179
+ GitHub output remains readable even though Nx release also has its own `preVersionCommand` safety net.
180
+
181
+ CI status deeplinks depend on GitHub's top-level job step anchors. The generated CI workflow keeps `# Step N` comments
182
+ next to each top-level step, and the `smoo github-ci nx-smart --step <number>` values for lint, test, and build must
183
+ stay synchronized with those comments. Composite action internals do not change the top-level step numbers.
184
+
185
+ Managed CI setup is split across local composite actions:
186
+
187
+ - `setup-devenv` installs Nix, restores the Nix cache segment, imports the store NAR, restores `.devenv`/`.direnv` only
188
+ after an exact Nix cache hit, enables Cachix, installs devenv, restores `node_modules`, and builds the shell.
189
+ - `save-nix-devenv` runs under `always()`, calls `smoo github-ci cleanup-cache`, and explicitly saves cache segments
190
+ only when cleanup reports `cache-ready=true`.
191
+ - `cache-nix-devenv` is the shared restore/save primitive for the separate `nix` and `devenv` cache segments.
192
+
193
+ The cache split is intentional. The Nix segment contains the profile, Nix state, and exported store NAR. It is large and
194
+ keyed by the expensive shell closure inputs. The `.devenv`/`.direnv` segment is small, but it contains absolute
195
+ `/nix/store` pointers, so restoring it without the exact matching Nix store can leave metadata pointing at missing store
196
+ paths.
197
+
198
+ `smoo github-ci cleanup-cache` prepares the Nix segment for saving. It verifies and repairs the store, scans `.devenv`,
199
+ `.direnv`, and `~/.nix-profile` for embedded `/nix/store/...` references, protects the live paths with temporary GC
200
+ roots, runs garbage collection, and exports the resulting closure to `NIX_STORE_NAR`. The command writes
201
+ `cache-ready=true` or `cache-ready=false` to `GITHUB_OUTPUT` so the save action can avoid uploading incomplete caches.
202
+
203
+ The bootstrap script is intentionally small. It only handles work required before `smoo` can run in GitHub Actions:
204
+
205
+ - Restore Nix store cache state, clearing `.devenv` and `.direnv` if the matching NAR is missing or fails to import.
206
+ - Install `devenv`.
207
+ - Build the devenv shell and add repo-local tooling to `GITHUB_PATH`.
208
+
209
+ ## Releases
210
+
211
+ Release commands wrap Nx release but keep SmoothBricks policy in one place.
212
+
213
+ Versioning:
214
+
215
+ - `--bump auto` uses Nx conventional-commit versioning.
216
+ - `--bump patch|minor|major|prerelease` forces the release specifier.
217
+ - Release projects are discovered from `npm:public` packages.
218
+ - Nx release config must use `currentVersionResolver: "git-tag"` with `fallbackCurrentVersionResolver: "disk"`.
219
+ Conventional-commit versioning requires git tags as the primary source, while the disk fallback supports initial
220
+ releases before package tags exist.
221
+ - `smoo release version` lets Nx own local package versioning, `bun.lock` updates, the release commit, and annotated
222
+ tags. smoo records `HEAD` before and after Nx runs, then pushes with `git push --follow-tags --atomic` only if Nx
223
+ created a new release commit. This keeps the release commit and tags atomic without pushing an older checkout on
224
+ retries where Nx has nothing new to commit.
225
+ - Reruns call Nx versioning again rather than repairing tags in `smoo`. Nx's git-tag current-version resolver plus disk
226
+ fallback handles already-tagged releases and first releases before package tags exist; smoo skips the remote push when
227
+ Nx leaves `HEAD` unchanged.
228
+
229
+ Publishing:
230
+
231
+ - `prerelease` publishes with npm dist-tag `next`.
232
+ - Stable bumps publish with npm dist-tag `latest`.
233
+ - Conflicting explicit dist-tags are rejected.
234
+ - `smoo release publish` checks every current `name@version` for `npm:public` packages before publishing. Already
235
+ published versions are skipped, so reruns after auth or network failures retry only the package versions npm does not
236
+ have yet.
237
+ - Publish uses `bun pm pack` to create package tarballs, then publishes those tarballs with latest npm CLI and
238
+ `--provenance`. Bun pack resolves internal `workspace:*` dependency ranges to real versions in the tarball manifest;
239
+ smoo fails before publish if a packed manifest still contains `workspace:`.
240
+ - npm CLI owns publish authentication. When trusted publishing is configured, npm uses GitHub Actions OIDC from the
241
+ workflow's `id-token: write` permission. Before trusted publishing exists, the managed workflow passes
242
+ `secrets.NPM_TOKEN` as `NODE_AUTH_TOKEN` and smoo writes a temporary npm user config for bootstrap publishing.
243
+ - `smoo release trust-publisher` configures npm trusted publishing for every `npm:public` package. It uses the root
244
+ `package.json` `repository.url` as the GitHub `owner/repo`, uses `publish.yml` as the trusted workflow, and runs
245
+ `npm trust` through `nix shell nixpkgs#nodejs_latest` because the Lambda-pinned Node 24/npm toolchain may lag the npm
246
+ CLI feature. By default it runs `npm login --auth-type=web` first so npm can open a browser login; pass `--skip-login`
247
+ when the current npm session is already authenticated. Packages must already exist on npm before trust can be
248
+ configured. npm may still require operation-level 2FA for `npm trust`; smoo prompts for a hidden OTP per package, or
249
+ you can pass `--otp <code>` for non-interactive use.
250
+
251
+ GitHub Releases:
252
+
253
+ - `smoo release github-release` creates or updates releases for the selected tags.
254
+ - `latest` status follows the derived npm dist-tag.
255
+
256
+ The release flow is designed to be rerun after partial failure. Nx owns local version/tag behavior, `smoo` owns the
257
+ guarded atomic git push, and npm registry state makes publishing idempotent across retries.
258
+
259
+ ## Why This Shape
260
+
261
+ The important design goal is one source of truth per convention:
262
+
263
+ - Nx `npm:public` tags decide what is publishable.
264
+ - Managed files decide what generated CI and hooks should look like.
265
+ - Root package metadata provides defaults for public packages.
266
+ - Actual workspace package names decide which dependency ranges become `workspace:*`.
267
+ - `sherif` handles broad package hygiene.
268
+ - `publint` and `attw` validate real packed artifacts.
269
+
270
+ This keeps `smoo` small where external tools already do the job, but keeps SmoothBricks-specific policy native where
271
+ generic tools do not know the repo contract. In particular, `sherif` is useful for package hygiene, but it does not know
272
+ SmoothBricks publish metadata, release tags, generated workflow files, or Nx release policy. Those remain `smoo`
273
+ conventions.
274
+
275
+ ## Local Verification
276
+
277
+ Typical verification after changing `smoo`:
278
+
279
+ ```bash
280
+ nx typecheck @smoothbricks/cli
281
+ nx lint @smoothbricks/cli
282
+ smoo monorepo validate
283
+ ```
284
+
285
+ If validating packages with Zig build steps from outside the devenv shell, add Zig explicitly:
286
+
287
+ ```bash
288
+ nix shell nixpkgs#zig -c nx run-many -t build --projects=<public-projects>
289
+ ```
package/bin/smoo ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bun
2
+ import { runCli } from '../dist/cli.js';
3
+
4
+ await runCli();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function runCli(argv?: string[]): Promise<void>;
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,wBAAsB,MAAM,CAAC,IAAI,WAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAexE"}
package/dist/cli.js ADDED
@@ -0,0 +1,141 @@
1
+ import { Command, CommanderError } from 'commander';
2
+ import { findRepoRoot } from './lib/run.js';
3
+ export async function runCli(argv = process.argv.slice(2)) {
4
+ const program = buildProgram();
5
+ try {
6
+ await program.parseAsync(argv, { from: 'user' });
7
+ }
8
+ catch (error) {
9
+ if (error instanceof CommanderError) {
10
+ if (error.code !== 'commander.helpDisplayed') {
11
+ process.exitCode = error.exitCode;
12
+ }
13
+ return;
14
+ }
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ console.error(message);
17
+ process.exitCode = 1;
18
+ }
19
+ }
20
+ function buildProgram() {
21
+ const program = new Command();
22
+ program.name('smoo').description('SmoothBricks monorepo tooling').exitOverride().showHelpAfterError();
23
+ const monorepo = program.command('monorepo').description('Manage SmoothBricks-style monorepos');
24
+ monorepo
25
+ .command('init')
26
+ .option('--runtime-only', 'only sync root Bun/Node runtime metadata')
27
+ .option('--sync-runtime', 'sync root Bun/Node runtime metadata outside devenv')
28
+ .action(async (options) => {
29
+ const { initMonorepo } = await import('./monorepo/index.js');
30
+ await initMonorepo(await findRepoRoot(), options);
31
+ });
32
+ monorepo
33
+ .command('validate')
34
+ .option('--fail-fast', 'stop after the first failing validation pack')
35
+ .action(async (options) => {
36
+ const { validateMonorepo } = await import('./monorepo/index.js');
37
+ await validateMonorepo(await findRepoRoot(), options);
38
+ });
39
+ monorepo.command('update').action(async () => {
40
+ const { updateManagedFiles } = await import('./monorepo/index.js');
41
+ updateManagedFiles(await findRepoRoot());
42
+ });
43
+ monorepo.command('check').action(async () => {
44
+ const { checkManagedFiles } = await import('./monorepo/index.js');
45
+ checkManagedFiles(await findRepoRoot());
46
+ });
47
+ monorepo.command('diff').action(async () => {
48
+ const { diffManagedFiles } = await import('./monorepo/index.js');
49
+ diffManagedFiles(await findRepoRoot());
50
+ });
51
+ monorepo.command('validate-commit-msg <commitMsgFile>').action(async (commitMsgFile) => {
52
+ const { validateCommitMessageFile } = await import('./monorepo/index.js');
53
+ validateCommitMessageFile(commitMsgFile);
54
+ });
55
+ monorepo.command('sync-bun-lockfile-versions').action(async () => {
56
+ const { syncBunLockfileVersions } = await import('./monorepo/index.js');
57
+ syncBunLockfileVersions(await findRepoRoot());
58
+ });
59
+ monorepo.command('list-public-projects').action(async () => {
60
+ const { listPublicProjects } = await import('./monorepo/index.js');
61
+ console.log(listPublicProjects(await findRepoRoot()));
62
+ });
63
+ monorepo.command('validate-public-tags').action(async () => {
64
+ const { validatePublicPackageTags } = await import('./monorepo/index.js');
65
+ validatePublicPackageTags(await findRepoRoot());
66
+ });
67
+ monorepo.command('release-state').action(async () => {
68
+ const { printReleaseState } = await import('./release/index.js');
69
+ await printReleaseState(await findRepoRoot());
70
+ });
71
+ const release = program.command('release').description('Version, publish, and create GitHub Releases');
72
+ release
73
+ .command('version')
74
+ .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
75
+ .option('--dry-run [dryRun]', 'run without writing versions, tags, or pushes')
76
+ .action(async (options) => {
77
+ const { releaseVersion } = await import('./release/index.js');
78
+ await releaseVersion(await findRepoRoot(), { bump: options.bump, dryRun: booleanOption(options.dryRun) });
79
+ });
80
+ release
81
+ .command('publish')
82
+ .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
83
+ .option('--tag <tag>', 'explicit npm dist-tag; must match bump-derived tag')
84
+ .option('--npm-tag <npmTag>', 'explicit npm dist-tag; must match bump-derived tag')
85
+ .option('--dry-run [dryRun]', 'run without publishing')
86
+ .action(async (options) => {
87
+ const { releasePublish } = await import('./release/index.js');
88
+ await releasePublish(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
89
+ });
90
+ release
91
+ .command('github-release')
92
+ .option('--tags <tags>', 'space-separated release tags')
93
+ .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
94
+ .option('--tag <tag>', 'explicit npm dist-tag; must match bump-derived tag')
95
+ .option('--npm-tag <npmTag>', 'explicit npm dist-tag; must match bump-derived tag')
96
+ .option('--dry-run [dryRun]', 'skip GitHub Release writes')
97
+ .action(async (options) => {
98
+ const { releaseGithubRelease } = await import('./release/index.js');
99
+ await releaseGithubRelease(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
100
+ });
101
+ release
102
+ .command('trust-publisher')
103
+ .description('Configure npm trusted publishing for public packages')
104
+ .option('--dry-run [dryRun]', 'show npm trust changes without saving them')
105
+ .option('--otp <otp>', 'npm one-time password for trust operations')
106
+ .option('--skip-login', 'skip npm browser login before configuring trust')
107
+ .action(async (options) => {
108
+ const { releaseTrustPublisher } = await import('./release/index.js');
109
+ await releaseTrustPublisher(await findRepoRoot(), {
110
+ dryRun: booleanOption(options.dryRun),
111
+ otp: options.otp,
112
+ skipLogin: options.skipLogin === true,
113
+ });
114
+ });
115
+ const githubCi = program.command('github-ci').description('GitHub Actions helpers');
116
+ githubCi.command('cleanup-cache').action(async () => {
117
+ const { cleanupGithubCiCache } = await import('./github-ci/index.js');
118
+ await cleanupGithubCiCache(await findRepoRoot());
119
+ });
120
+ githubCi
121
+ .command('nx-smart')
122
+ .requiredOption('--target <target>')
123
+ .option('--name <name>')
124
+ .option('--step <step>')
125
+ .action(async (options) => {
126
+ const { githubCiNxSmart } = await import('./github-ci/index.js');
127
+ await githubCiNxSmart(await findRepoRoot(), options);
128
+ });
129
+ githubCi
130
+ .command('nx-run-many')
131
+ .requiredOption('--targets <targets>')
132
+ .option('--projects <projects>')
133
+ .action(async (options) => {
134
+ const { githubCiNxRunMany } = await import('./github-ci/index.js');
135
+ await githubCiNxRunMany(await findRepoRoot(), options);
136
+ });
137
+ return program;
138
+ }
139
+ function booleanOption(value) {
140
+ return value === true || value === 'true';
141
+ }
@@ -0,0 +1,11 @@
1
+ export declare function cleanupGithubCiCache(root: string): Promise<void>;
2
+ export declare function githubCiNxSmart(root: string, options: {
3
+ target: string;
4
+ name?: string;
5
+ step?: string;
6
+ }): Promise<void>;
7
+ export declare function githubCiNxRunMany(root: string, options: {
8
+ targets: string;
9
+ projects?: string;
10
+ }): Promise<void>;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/github-ci/index.ts"],"names":[],"mappings":"AAMA,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBtE;AAmFD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAMpH"}
@@ -0,0 +1,174 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { appendFile, mkdtemp, realpath, rename, rm } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { $ } from 'bun';
5
+ import { decode, run, runStatus } from '../lib/run.js';
6
+ export async function cleanupGithubCiCache(root) {
7
+ const githubOutput = process.env.GITHUB_OUTPUT;
8
+ const markCacheReady = async (ready) => {
9
+ if (githubOutput) {
10
+ await appendFile(githubOutput, `cache-ready=${ready ? 'true' : 'false'}\n`);
11
+ }
12
+ };
13
+ const nar = process.env.NIX_STORE_NAR;
14
+ if (!nar) {
15
+ console.warn('NIX_STORE_NAR is not set; skipping Nix cache save.');
16
+ await markCacheReady(false);
17
+ return;
18
+ }
19
+ const nixStore = '/nix/var/nix/profiles/default/bin/nix-store';
20
+ const devenvProfile = `${root}/tooling/direnv/.devenv/profile`;
21
+ if (!existsSync(devenvProfile)) {
22
+ console.warn(`${devenvProfile} is missing; skipping Nix cache save.`);
23
+ await markCacheReady(false);
24
+ return;
25
+ }
26
+ await runStatus(nixStore, ['--verify', '--check-contents', '--repair'], root);
27
+ await exportNixStoreCache(root, nar, nixStore, devenvProfile);
28
+ await markCacheReady(true);
29
+ }
30
+ async function exportNixStoreCache(root, nar, nixStore, devenvProfile) {
31
+ const gcRootDir = '/nix/var/nix/gcroots/smoothbricks-cache-roots';
32
+ const tmpDir = await mkdtemp(join(dirname(nar), '.smoo-nix-cache-'));
33
+ const tmpNar = join(tmpDir, 'nix-store.nar');
34
+ const roots = new Set();
35
+ try {
36
+ await $ `rm -f ${nar}`.cwd(root);
37
+ await $ `sudo rm -rf ${gcRootDir}`.cwd(root);
38
+ await $ `sudo mkdir -p ${gcRootDir}`.cwd(root);
39
+ // The Nix cache must include every live store path referenced by the
40
+ // restored shell state, not just the devenv profile. .direnv stores paths to
41
+ // derivations like devenv-shell.drv; omitting those makes a cache hit restore
42
+ // metadata that points at missing store paths.
43
+ await addRoot(roots, devenvProfile);
44
+ const home = process.env.HOME;
45
+ if (home) {
46
+ const nixProfile = join(home, '.nix-profile');
47
+ await addRoot(roots, nixProfile);
48
+ await addReferencesFrom(roots, nixProfile, root);
49
+ }
50
+ await addReferencesFrom(roots, join(root, 'tooling/direnv/.devenv'), root);
51
+ await addReferencesFrom(roots, join(root, 'tooling/direnv/.direnv'), root);
52
+ const rootLinks = [];
53
+ let index = 0;
54
+ for (const target of roots) {
55
+ const link = `${gcRootDir}/root-${index}`;
56
+ await $ `sudo ln -s ${target} ${link}`.cwd(root);
57
+ rootLinks.push(link);
58
+ index += 1;
59
+ }
60
+ if (rootLinks.length === 0) {
61
+ throw new Error('No live Nix store roots found; skipping Nix cache save.');
62
+ }
63
+ await $ `nix-collect-garbage --quiet`.cwd(root);
64
+ const closureOutput = await $ `sudo ${nixStore} -qR ${rootLinks}`.cwd(root).quiet();
65
+ const closure = decode(closureOutput.stdout)
66
+ .split('\n')
67
+ .map((line) => line.trim())
68
+ .filter(Boolean);
69
+ if (closure.length === 0) {
70
+ throw new Error('No Nix store closure paths found; skipping Nix cache save.');
71
+ }
72
+ await $ `sudo ${nixStore} --export --quiet ${closure} > ${tmpNar}`.cwd(root);
73
+ await $ `test -s ${tmpNar}`.cwd(root);
74
+ await rename(tmpNar, nar);
75
+ }
76
+ finally {
77
+ await $ `sudo rm -rf ${gcRootDir}`.cwd(root).nothrow();
78
+ await rm(tmpDir, { recursive: true, force: true });
79
+ }
80
+ }
81
+ async function addRoot(roots, candidate) {
82
+ if (!existsSync(candidate)) {
83
+ return;
84
+ }
85
+ const target = await realpath(candidate);
86
+ if (existsSync(target)) {
87
+ roots.add(target);
88
+ }
89
+ }
90
+ async function addReferencesFrom(roots, path, cwd) {
91
+ if (!existsSync(path)) {
92
+ return;
93
+ }
94
+ const storePathPattern = '/nix/store/[a-z0-9]{32}-[A-Za-z0-9+._?=-]+';
95
+ const result = await $ `grep -rahoE ${storePathPattern} ${path}`.cwd(cwd).quiet().nothrow();
96
+ for (const line of decode(result.stdout).split('\n')) {
97
+ const candidate = line.trim();
98
+ if (candidate && existsSync(candidate)) {
99
+ roots.add(candidate);
100
+ }
101
+ }
102
+ }
103
+ export async function githubCiNxSmart(root, options) {
104
+ const name = options.name ?? options.target;
105
+ const step = options.step ?? '';
106
+ await createGithubStatus(name, step);
107
+ const mode = process.env.GITHUB_EVENT_NAME === 'push' && process.env.GITHUB_REF_NAME === 'main' ? 'run-many' : 'affected';
108
+ const nxArgs = mode === 'run-many'
109
+ ? ['run-many', '-t', options.target, '--parallel=5']
110
+ : ['affected', '-t', options.target, '--parallel=5'];
111
+ const status = await runStatus('nx', nxArgs, root);
112
+ await updateGithubStatus(name, status === 0 ? 'success' : 'failure', step);
113
+ if (status !== 0) {
114
+ throw new Error(`nx ${nxArgs.join(' ')} failed with exit code ${status}`);
115
+ }
116
+ }
117
+ export async function githubCiNxRunMany(root, options) {
118
+ const nxArgs = ['run-many', '-t', options.targets, '--parallel=5'];
119
+ if (options.projects) {
120
+ nxArgs.push(`--projects=${options.projects}`);
121
+ }
122
+ await run('nx', nxArgs, root);
123
+ }
124
+ async function createGithubStatus(name, step) {
125
+ await postGithubStatus(name, 'pending', `Running ${name}...`, step);
126
+ }
127
+ async function updateGithubStatus(name, state, step) {
128
+ const suffix = state === 'success' ? 'passed' : state === 'failure' ? 'failed' : 'errored';
129
+ await postGithubStatus(name, state, `${name} ${suffix}`, step);
130
+ }
131
+ async function postGithubStatus(name, state, description, step) {
132
+ const repository = process.env.GITHUB_REPOSITORY;
133
+ const sha = process.env.GITHUB_SHA;
134
+ if (!repository || !sha) {
135
+ return;
136
+ }
137
+ const targetUrl = await getGithubStepUrl(step);
138
+ const args = [
139
+ 'api',
140
+ '--method',
141
+ 'POST',
142
+ '-H',
143
+ 'Accept: application/vnd.github+json',
144
+ `/repos/${repository}/statuses/${sha}`,
145
+ '-f',
146
+ `state=${state}`,
147
+ '-f',
148
+ `context=${name}`,
149
+ '-f',
150
+ `description=${description}`,
151
+ ];
152
+ if (targetUrl) {
153
+ args.push('-f', `target_url=${targetUrl}`);
154
+ }
155
+ await run('gh', args, process.cwd());
156
+ }
157
+ async function getGithubStepUrl(step) {
158
+ const repository = process.env.GITHUB_REPOSITORY;
159
+ const runId = process.env.GITHUB_RUN_ID;
160
+ const job = process.env.GITHUB_JOB;
161
+ if (!repository || !runId || !job) {
162
+ return null;
163
+ }
164
+ const result = await $ `gh api -H ${'Accept: application/vnd.github+json'} ${`/repos/${repository}/actions/runs/${runId}/jobs`} --jq ${`.jobs[] | select(.name == "${job}") | .id`}`
165
+ .quiet()
166
+ .nothrow();
167
+ const jobId = decode(result.stdout).trim();
168
+ if (!jobId) {
169
+ return `https://github.com/${repository}/actions/runs/${runId}`;
170
+ }
171
+ return step
172
+ ? `https://github.com/${repository}/actions/runs/${runId}/job/${jobId}#step:${step}:1`
173
+ : `https://github.com/${repository}/actions/runs/${runId}/job/${jobId}`;
174
+ }
@@ -0,0 +1,2 @@
1
+ export { runCli } from './cli.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { runCli } from './cli.js';
@@ -0,0 +1,13 @@
1
+ import { hasOwn, hasOwnString, isRecord } from '@smoothbricks/validation';
2
+ export { hasOwn, hasOwnString, isRecord };
3
+ export declare function stringProperty(record: Record<string, unknown>, key: string): string | null;
4
+ export declare function recordProperty(record: Record<string, unknown>, key: string): Record<string, unknown> | null;
5
+ export declare function getOrCreateRecord(record: Record<string, unknown>, key: string): Record<string, unknown>;
6
+ export declare function setStringProperty(record: Record<string, unknown>, key: string, value: string): boolean;
7
+ export declare function setMissingStringProperty(record: Record<string, unknown>, key: string, value: string): boolean;
8
+ export declare function requiredJsonObject(path: string): Record<string, unknown>;
9
+ export declare function readJsonObject(path: string): Record<string, unknown> | null;
10
+ export declare function writeJsonObject(path: string, value: Record<string, unknown>): void;
11
+ export declare function jsonObjectText(value: Record<string, unknown>): string;
12
+ export declare function readJson(path: string): unknown;
13
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/lib/json.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAE1E,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AAE1C,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG1F;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAG3G;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQvG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMtG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM7G;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMxE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAG3E;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAElF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAK9C"}