@smoothbricks/cli 0.10.3 → 0.10.5

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 (115) hide show
  1. package/README.md +9 -13
  2. package/dist/bun/preload.d.ts +13 -0
  3. package/dist/bun/preload.d.ts.map +1 -0
  4. package/dist/bun/preload.js +12 -0
  5. package/dist/cli.js +13 -5
  6. package/dist/generate/index.d.ts.map +1 -1
  7. package/dist/generate/index.js +0 -6
  8. package/dist/github-ci/index.d.ts.map +1 -1
  9. package/dist/github-ci/index.js +52 -16
  10. package/dist/github-ci/outputs.d.ts.map +1 -1
  11. package/dist/github-ci/outputs.js +1 -2
  12. package/dist/lib/cli-package.js +2 -2
  13. package/dist/lib/json.d.ts +120 -11
  14. package/dist/lib/json.d.ts.map +1 -1
  15. package/dist/lib/json.js +319 -29
  16. package/dist/lib/workspace.d.ts +7 -14
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +76 -82
  19. package/dist/monorepo/ci-workflow.d.ts +2 -0
  20. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  21. package/dist/monorepo/ci-workflow.js +27 -1
  22. package/dist/monorepo/git-config.js +2 -2
  23. package/dist/monorepo/index.d.ts +4 -2
  24. package/dist/monorepo/index.d.ts.map +1 -1
  25. package/dist/monorepo/index.js +19 -3
  26. package/dist/monorepo/lockfile.d.ts +14 -1
  27. package/dist/monorepo/lockfile.d.ts.map +1 -1
  28. package/dist/monorepo/lockfile.js +33 -21
  29. package/dist/monorepo/managed-files.d.ts +11 -0
  30. package/dist/monorepo/managed-files.d.ts.map +1 -1
  31. package/dist/monorepo/managed-files.js +57 -32
  32. package/dist/monorepo/package-policy.d.ts.map +1 -1
  33. package/dist/monorepo/package-policy.js +52 -45
  34. package/dist/monorepo/packed-manifest.d.ts +5 -2
  35. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  36. package/dist/monorepo/packed-manifest.js +16 -47
  37. package/dist/monorepo/packed-package.d.ts.map +1 -1
  38. package/dist/monorepo/packed-package.js +28 -16
  39. package/dist/monorepo/packs/index.d.ts.map +1 -1
  40. package/dist/monorepo/packs/index.js +14 -5
  41. package/dist/monorepo/publish-workflow.js +3 -1
  42. package/dist/monorepo/runtime.d.ts +15 -0
  43. package/dist/monorepo/runtime.d.ts.map +1 -1
  44. package/dist/monorepo/runtime.js +66 -12
  45. package/dist/monorepo/tool-validation.d.ts +2 -2
  46. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  47. package/dist/monorepo/tool-validation.js +141 -38
  48. package/dist/monorepo/wrangler.d.ts.map +1 -1
  49. package/dist/monorepo/wrangler.js +13 -13
  50. package/dist/nx/index.d.ts +9 -7
  51. package/dist/nx/index.d.ts.map +1 -1
  52. package/dist/nx/index.js +48 -31
  53. package/dist/pr/index.d.ts.map +1 -1
  54. package/dist/pr/index.js +28 -32
  55. package/dist/release/candidates.d.ts +3 -2
  56. package/dist/release/candidates.d.ts.map +1 -1
  57. package/dist/release/candidates.js +4 -5
  58. package/dist/release/index.d.ts +1 -0
  59. package/dist/release/index.d.ts.map +1 -1
  60. package/dist/release/index.js +88 -59
  61. package/dist/wrangler/prepare-env.d.ts +0 -1
  62. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  63. package/dist/wrangler/prepare-env.js +100 -51
  64. package/dist/wrangler/scaffold.d.ts.map +1 -1
  65. package/dist/wrangler/scaffold.js +12 -12
  66. package/managed/raw/envrc +5 -3
  67. package/managed/raw/git-format-staged.yml +19 -1
  68. package/managed/raw/patches/ttsc@0.19.3.patch +67 -0
  69. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +2 -1
  70. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  71. package/managed/raw/tooling/direnv/setup-environment.ts +170 -19
  72. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  73. package/managed/templates/github/actions/cache-node-modules/action.yml +5 -2
  74. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  75. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  76. package/managed/templates/github/workflows/managed-files.yml +80 -0
  77. package/package.json +15 -4
  78. package/src/bun/preload.ts +12 -0
  79. package/src/cli.ts +20 -8
  80. package/src/generate/index.ts +0 -6
  81. package/src/github-ci/index.ts +49 -18
  82. package/src/github-ci/outputs.ts +1 -2
  83. package/src/lib/cli-package.ts +2 -2
  84. package/src/lib/json.ts +293 -32
  85. package/src/lib/workspace.ts +88 -100
  86. package/src/monorepo/__tests__/ci-workflow.test.ts +3 -3
  87. package/src/monorepo/ci-workflow.ts +27 -1
  88. package/src/monorepo/git-config.ts +2 -2
  89. package/src/monorepo/index.ts +19 -3
  90. package/src/monorepo/lockfile.test.ts +35 -36
  91. package/src/monorepo/lockfile.ts +49 -24
  92. package/src/monorepo/managed-files.test.ts +1 -0
  93. package/src/monorepo/managed-files.ts +62 -38
  94. package/src/monorepo/package-policy.test.ts +20 -50
  95. package/src/monorepo/package-policy.ts +76 -59
  96. package/src/monorepo/packed-manifest.ts +18 -52
  97. package/src/monorepo/packed-package.ts +43 -22
  98. package/src/monorepo/packs/index.ts +14 -5
  99. package/src/monorepo/publish-workflow.ts +3 -1
  100. package/src/monorepo/runtime.test.ts +42 -3
  101. package/src/monorepo/runtime.ts +90 -12
  102. package/src/monorepo/tool-validation.test.ts +38 -16
  103. package/src/monorepo/tool-validation.ts +149 -41
  104. package/src/monorepo/wrangler.test.ts +11 -15
  105. package/src/monorepo/wrangler.ts +15 -15
  106. package/src/nx/index.test.ts +9 -5
  107. package/src/nx/index.ts +69 -42
  108. package/src/pr/index.ts +42 -36
  109. package/src/release/__tests__/candidates.test.ts +5 -4
  110. package/src/release/__tests__/trust-publisher.test.ts +7 -0
  111. package/src/release/candidates.ts +10 -17
  112. package/src/release/index.ts +110 -73
  113. package/src/wrangler/prepare-env.ts +75 -54
  114. package/src/wrangler/scaffold.test.ts +9 -12
  115. package/src/wrangler/scaffold.ts +13 -13
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
3
3
  import { mkdir, rmdir, stat } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
4
5
  import path from 'node:path';
5
6
  import { $ } from 'bun';
6
7
 
7
8
  const devenvRoot = process.env.DEVENV_ROOT;
8
9
  const projectRoot = path.resolve(`${devenvRoot}/../..`);
9
- const startedWithoutNodeModules = !existsSync(path.join(projectRoot, 'node_modules'));
10
10
 
11
11
  // A legitimate concurrent setup finishes well within this; anything older is a
12
12
  // leftover from an interrupted run (CTRL-C/kill before the finally-cleanup) and
@@ -14,6 +14,13 @@ const startedWithoutNodeModules = !existsSync(path.join(projectRoot, 'node_modul
14
14
  // NOTE: must be declared ABOVE the top-level setup block below — module consts
15
15
  // are not hoisted, and the lock loop runs during that block.
16
16
  const STALE_LOCK_MS = 10 * 60_000;
17
+ // Unscoped require("typescript") must expose the TS6 compiler API for Nx.
18
+ // @typescript/native installs another package also named "typescript" (TS7). Bun's
19
+ // isolated linker often points node_modules/.bun/node_modules/typescript at TS7,
20
+ // so packages resolved from the .bun store (nx) get version.cjs without readConfigFile.
21
+ // After every install, force both unscoped typescript slots onto typescript@6.0.3.
22
+ // Keep @typescript/native for ttsc via TTSC_TSGO_BINARY. See https://github.com/oven-sh/bun/issues/33834.
23
+ const TYPESCRIPT_API_VERSION = '6.0.3';
17
24
 
18
25
  class CapturedCommandError extends Error {
19
26
  constructor(
@@ -30,7 +37,9 @@ class CapturedCommandError extends Error {
30
37
  process.chdir(projectRoot);
31
38
 
32
39
  try {
33
- // Install dependencies first so node_modules/.bin tools are available
40
+ // Bootstrap only: install deps + wire local git hooks/config.
41
+ // Do not import workspace packages here — this script is what installs them,
42
+ // and package resolution/Typia transforms are not available yet.
34
43
  if (process.env.CI) {
35
44
  try {
36
45
  await runSetupCommand('bun install --frozen-lockfile', $`bun install --frozen-lockfile`, { quiet: false });
@@ -46,20 +55,9 @@ try {
46
55
  await installLocalDependencies();
47
56
  }
48
57
 
49
- // Bun selects its resolver mode at process startup. If this process started
50
- // before node_modules existed, imports below can stay in auto-install mode
51
- // even after bun install succeeds, so restart once into the real workspace.
52
- if (startedWithoutNodeModules) {
53
- await runSetupCommand('restart setup-environment.ts', $`bun --no-install ${import.meta.path}`, { quiet: false });
54
- process.exit(0);
55
- }
56
-
57
- if (!process.env.CI) {
58
- const { syncRootRuntimeVersions } = await import('@smoothbricks/cli/monorepo/runtime');
59
- await syncRootRuntimeVersions(projectRoot);
60
- }
58
+ // Pin unscoped typescript API 6 for root and Bun's shared .bun hoist (Nx).
59
+ ensureTypeScriptApiPackage(projectRoot);
61
60
 
62
- const { applyWorkspaceGitConfig } = await import('@smoothbricks/cli/monorepo/git-config');
63
61
  await applyWorkspaceGitConfig(projectRoot);
64
62
  } catch (error) {
65
63
  if (error instanceof CapturedCommandError) {
@@ -74,14 +72,167 @@ try {
74
72
  }
75
73
 
76
74
  async function installLocalDependencies(): Promise<void> {
77
- // bun install runs the root prepare script, which patches TypeScript with
78
- // ts-patch. Multiple concurrent direnv activations can otherwise race while
79
- // mutating the same files under node_modules.
75
+ // bun install runs the root prepare script. Multiple concurrent direnv
76
+ // activations can otherwise race while mutating the same files under
77
+ // node_modules.
80
78
  await withSetupLock(async () => {
81
79
  await runSetupCommand('bun install --no-summary', $`bun install --no-summary`);
82
80
  });
83
81
  }
84
82
 
83
+ function ensureTypeScriptApiPackage(root: string): void {
84
+ const apiPackageRoot = findInstalledTypeScriptApiPackage(root);
85
+ if (!apiPackageRoot) {
86
+ throw new Error(
87
+ `typescript@${TYPESCRIPT_API_VERSION} was not installed under node_modules/.bun. ` +
88
+ `Keep root devDependency typescript@^${TYPESCRIPT_API_VERSION} (compiler API for Nx).`,
89
+ );
90
+ }
91
+
92
+ // Root bare require("typescript") and Bun store-local requires (nx lives under
93
+ // node_modules/.bun/...) must both see the API package — not @typescript/native's TS7.
94
+ forceSymlink(path.join(root, 'node_modules', 'typescript'), apiPackageRoot);
95
+ forceSymlink(path.join(root, 'node_modules', '.bun', 'node_modules', 'typescript'), apiPackageRoot);
96
+
97
+ assertTypescriptApiAt(path.join(root, 'node_modules', 'typescript'), apiPackageRoot);
98
+ assertTypescriptApiAt(path.join(root, 'node_modules', '.bun', 'node_modules', 'typescript'), apiPackageRoot);
99
+
100
+ const nativeBin = path.join(root, 'node_modules', '@typescript', 'native', 'bin', 'tsc');
101
+ const rootPackageJson = path.join(root, 'package.json');
102
+ const declaresNative =
103
+ existsSync(rootPackageJson) &&
104
+ (() => {
105
+ try {
106
+ const pkg = JSON.parse(readFileSync(rootPackageJson, 'utf8')) as {
107
+ devDependencies?: Record<string, string>;
108
+ dependencies?: Record<string, string>;
109
+ };
110
+ return (
111
+ typeof pkg.devDependencies?.['@typescript/native'] === 'string' ||
112
+ typeof pkg.dependencies?.['@typescript/native'] === 'string'
113
+ );
114
+ } catch {
115
+ return false;
116
+ }
117
+ })();
118
+ if (declaresNative && !existsSync(nativeBin)) {
119
+ throw new Error(
120
+ `Missing ${nativeBin}. package.json declares @typescript/native but it is not installed. ` +
121
+ 'Run bun install (or direnv reload). @typescript/native is the TypeScript 7 native compiler for ttsc.',
122
+ );
123
+ }
124
+ }
125
+
126
+ function findInstalledTypeScriptApiPackage(root: string): string | null {
127
+ const bunStore = path.join(root, 'node_modules', '.bun');
128
+ if (!existsSync(bunStore)) {
129
+ return null;
130
+ }
131
+ const exact = `typescript@${TYPESCRIPT_API_VERSION}`;
132
+ const names = readdirSync(bunStore)
133
+ .filter((name) => name === exact || name.startsWith(`${exact}+`) || name.startsWith('typescript@6.'))
134
+ .sort((a, b) => {
135
+ const aExact = a === exact || a.startsWith(`${exact}+`) ? 0 : 1;
136
+ const bExact = b === exact || b.startsWith(`${exact}+`) ? 0 : 1;
137
+ return aExact - bExact || a.localeCompare(b);
138
+ });
139
+ for (const name of names) {
140
+ const candidate = path.join(bunStore, name, 'node_modules', 'typescript');
141
+ if (existsSync(path.join(candidate, 'package.json'))) {
142
+ return candidate;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function forceSymlink(linkPath: string, targetPath: string): void {
149
+ mkdirSync(path.dirname(linkPath), { recursive: true });
150
+ const relativeTarget = path.relative(path.dirname(linkPath), targetPath);
151
+ let current: string | null = null;
152
+ try {
153
+ current = readlinkSync(linkPath);
154
+ } catch {
155
+ current = null;
156
+ }
157
+ if (
158
+ current === relativeTarget ||
159
+ (current !== null && path.resolve(path.dirname(linkPath), current) === targetPath)
160
+ ) {
161
+ return;
162
+ }
163
+ rmSync(linkPath, { recursive: true, force: true });
164
+ symlinkSync(relativeTarget, linkPath);
165
+ }
166
+
167
+ function assertTypescriptApiAt(typescriptRoot: string, expectedTarget: string): void {
168
+ if (!existsSync(typescriptRoot)) {
169
+ throw new Error(`Missing ${typescriptRoot} after linking TypeScript ${TYPESCRIPT_API_VERSION} API package`);
170
+ }
171
+ const requireFromInstall = createRequire(path.join(typescriptRoot, 'package.json'));
172
+ const typed = requireFromInstall(typescriptRoot) as { version?: string; readConfigFile?: unknown };
173
+ if (typeof typed.readConfigFile !== 'function' || typed.version !== TYPESCRIPT_API_VERSION) {
174
+ throw new Error(
175
+ `${typescriptRoot} must export TypeScript ${TYPESCRIPT_API_VERSION} compiler API (readConfigFile). ` +
176
+ `Resolved version ${typed.version ?? 'unknown'} (expected link target ${expectedTarget}). ` +
177
+ 'Bun isolated linking often points .bun/node_modules/typescript at @typescript/native (TS7). ' +
178
+ 'See https://github.com/oven-sh/bun/issues/33834.',
179
+ );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Keep git hook wiring local to bootstrap. Runtime pin sync
185
+ * (`syncRootRuntimeVersions`) belongs to explicit monorepo tooling after the
186
+ * package graph exists — not the installer.
187
+ */
188
+ async function applyWorkspaceGitConfig(root: string): Promise<void> {
189
+ const gitDirResult = await $`git rev-parse --git-dir`.cwd(root).quiet().nothrow();
190
+ if (gitDirResult.exitCode !== 0) {
191
+ throw new Error('Not in a git repository');
192
+ }
193
+
194
+ const gitDir = path.resolve(root, new TextDecoder().decode(gitDirResult.stdout).trim());
195
+ const tooling = path.join(root, 'tooling');
196
+
197
+ await $`git config --local include.path ${path.join(tooling, 'workspace.gitconfig')}`.cwd(root);
198
+
199
+ // Keep the newer runtime version pins on any merge (nvfetcher overlay +
200
+ // devenv.lock) so a mirror sync's `git am --3way` never stalls on a version
201
+ // conflict. Mapped by the managed .gitattributes (merge=smoo-newer-pins);
202
+ // implemented in tooling/direnv/merge-newer-pins.sh.
203
+ await $`git config --local merge.smoo-newer-pins.name ${'keep the newer devenv/nvfetcher runtime pins'}`.cwd(root);
204
+ await $`git config --local merge.smoo-newer-pins.driver ${'bash tooling/direnv/merge-newer-pins.sh %O %A %B %P'}`.cwd(
205
+ root,
206
+ );
207
+ linkHook(gitDir, tooling, 'pre-commit');
208
+ linkHook(gitDir, tooling, 'commit-msg');
209
+ }
210
+
211
+ function linkHook(gitDir: string, tooling: string, name: string): void {
212
+ const source = path.join(tooling, 'git-hooks', `${name}.sh`);
213
+ if (!existsSync(source)) {
214
+ throw new Error(`Missing ${name} hook source: ${source}`);
215
+ }
216
+
217
+ const target = path.join(gitDir, 'hooks', name);
218
+ if (readLinkOrNull(target) === source) {
219
+ return;
220
+ }
221
+
222
+ console.log(`[!] Linking ${name} hook in ${gitDir}`);
223
+ mkdirSync(path.dirname(target), { recursive: true });
224
+ rmSync(target, { force: true });
225
+ symlinkSync(source, target);
226
+ }
227
+
228
+ function readLinkOrNull(hookPath: string): string | null {
229
+ try {
230
+ return readlinkSync(hookPath);
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
85
236
  async function runSetupCommand(
86
237
  command: string,
87
238
  shell: ReturnType<typeof $>,
@@ -9,12 +9,6 @@ set -e -o pipefail
9
9
  # Format exactly what is staged so the commit includes the formatter output.
10
10
  git-format-staged
11
11
 
12
- # Any `bun install` rewrites bun.lock workspace versions from package.json,
13
- # reverting the stable-tag versions the release flow syncs (see
14
- # packages/cli/src/monorepo/lockfile.ts). Self-heal here instead of failing a
15
- # later commit on drift it didn't cause.
16
- smoo monorepo sync-bun-lockfile-versions --stage
17
-
18
12
  # Adding a workspace package is rare and easy to leave half-wired. Run the
19
13
  # full monorepo validator only for newly staged package manifests.
20
14
  smoo monorepo validate --fail-fast --only-if-new-workspace-package
@@ -7,8 +7,11 @@ runs:
7
7
  - name: Cache node_modules
8
8
  uses: actions/cache@v5.0.5
9
9
  with:
10
+ # Dependencies only. ttsc plugin/GOCACHE live under $TTSC_CACHE_DIR
11
+ # (.cache/ttsc), not inside node_modules.
12
+ # v2 isolates restore-keys from older Darwin caches with a broken TS dual-package hoist.
10
13
  path: node_modules
11
14
  # prettier-ignore
12
- key: ${{ runner.os }}-${{ runner.arch }}-node-{{NODE_MODULES_CACHE_KEY}}
15
+ key: ${{ runner.os }}-${{ runner.arch }}-node-v2-{{NODE_MODULES_CACHE_KEY}}
13
16
  restore-keys: |
14
- ${{ runner.os }}-${{ runner.arch }}-node-
17
+ ${{ runner.os }}-${{ runner.arch }}-node-v2-
@@ -0,0 +1,19 @@
1
+ name: Cache ttsc plugins
2
+ description: >
3
+ Restore/save compiled ttsc native plugin binaries (typia, etc.). These are content-keyed under TTSC_CACHE_DIR/plugins
4
+ and skip the multi-minute `go build` when the cache key matches. The Go object cache (go-build/) is NOT cached — it is
5
+ multi-GB and only helps when the plugin key misses.
6
+
7
+ runs:
8
+ using: composite
9
+ steps:
10
+ - name: Cache ttsc plugin binaries
11
+ uses: actions/cache@v5.0.5
12
+ with:
13
+ # Only the compiled plugin binaries (~30MB each). Path is relative to the
14
+ # workspace; devenv sets TTSC_CACHE_DIR to <repo>/.cache/ttsc.
15
+ path: .cache/ttsc/plugins
16
+ # prettier-ignore
17
+ key: ${{ runner.os }}-${{ runner.arch }}-ttsc-plugins-v1-${{ hashFiles('bun.lock', 'package.json', 'patches/ttsc@*.patch', 'tooling/direnv/devenv.lock') }}
18
+ restore-keys: |
19
+ ${{ runner.os }}-${{ runner.arch }}-ttsc-plugins-v1-
@@ -51,6 +51,11 @@ runs:
51
51
  - name: 📦 Cache node_modules
52
52
  uses: ./.github/actions/cache-node-modules
53
53
 
54
+ # Restores compiled ttsc native plugins into .cache/ttsc/plugins. TTSC_CACHE_DIR
55
+ # is set by devenv enterShell (+ bootstrap GITHUB_ENV for later steps).
56
+ - name: 🧊 Cache ttsc plugins
57
+ uses: ./.github/actions/cache-ttsc-plugins
58
+
54
59
  - name: 🐚 Build devenv shell
55
60
  shell: bash
56
61
  working-directory: tooling/direnv
@@ -0,0 +1,80 @@
1
+ # Managed by @smoothbricks/cli (smoo monorepo update) - do not edit by hand.
2
+ #
3
+ # Self-healing loop for smoo-managed files: ci.yml runs the authoritative
4
+ # `smoo monorepo check` on every default-branch push (devenv is already up
5
+ # there, so the check is ~free) and dispatches THIS workflow only when real
6
+ # drift exists - typically after a @smoothbricks/cli pin bump. It runs
7
+ # `smoo monorepo update` and pushes the result to ONE persistent review branch
8
+ # (chore/managed-files) with ONE PR. The PR is NEVER auto-merged: a template
9
+ # change is upstream code that deserves a human look before it lands. Re-runs
10
+ # refresh the same branch in place.
11
+ #
12
+ # NOTE: managed files include .github/workflows/*, and the default GITHUB_TOKEN
13
+ # is server-side forbidden to push workflow-file changes. Configure a GitHub App
14
+ # or PAT token with contents:write + workflows:write as MANAGED_FILES_TOKEN;
15
+ # without it, runs that touch workflow files fail at push with a clear error.
16
+ name: Managed files
17
+
18
+ on:
19
+ workflow_dispatch:
20
+
21
+ permissions:
22
+ contents: write
23
+ pull-requests: write
24
+
25
+ concurrency:
26
+ group: managed-files
27
+ cancel-in-progress: true
28
+
29
+ defaults:
30
+ run:
31
+ working-directory: tooling/direnv
32
+
33
+ jobs:
34
+ update:
35
+ runs-on: ubuntu-latest
36
+ timeout-minutes: 30
37
+ steps:
38
+ - name: 📥 Checkout
39
+ uses: actions/checkout@v6.0.2
40
+ with:
41
+ filter: blob:none
42
+ fetch-depth: 0
43
+ persist-credentials: true
44
+ token: ${{ secrets.MANAGED_FILES_TOKEN || github.token }}
45
+
46
+ - name: 🧱 Setup Nix/devenv
47
+ id: setup
48
+ uses: ./.github/actions/setup-devenv
49
+
50
+ - name: 🔄 Regenerate managed files
51
+ run: smoo monorepo update
52
+
53
+ - name: 📤 Push review branch and open PR
54
+ env:
55
+ GH_TOKEN: ${{ secrets.MANAGED_FILES_TOKEN || github.token }}
56
+ run: |
57
+ cd "$(git rev-parse --show-toplevel)"
58
+ if [ -z "$(git status --porcelain)" ]; then
59
+ echo "Managed files are in sync; nothing to do."
60
+ exit 0
61
+ fi
62
+ git config user.name "github-actions[bot]"
63
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
64
+ git switch -C chore/managed-files
65
+ git add -A
66
+ git commit -m "chore: regenerate smoo-managed files"
67
+ git push --force origin chore/managed-files
68
+ gh pr create \
69
+ --head chore/managed-files \
70
+ --base "${GITHUB_REF_NAME}" \
71
+ --title "chore: regenerate smoo-managed files" \
72
+ --body "Automated refresh of smoo-managed files after drift (usually a @smoothbricks/cli pin bump). One persistent PR; re-runs force-push this branch in place. Review the template changes - do NOT enable auto-merge." \
73
+ 2>/dev/null || echo "PR already open; branch refreshed in place."
74
+
75
+ - name: 🧹 Cleanup and cache Nix/devenv
76
+ if: ${{ always() }}
77
+ uses: ./.github/actions/save-nix-devenv
78
+ with:
79
+ nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}
80
+ devenv-cache-hit: ${{ steps.setup.outputs.devenv-cache-hit }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.10.3",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -20,6 +20,13 @@
20
20
  "import": "./dist/index.js",
21
21
  "default": "./dist/index.js"
22
22
  },
23
+ "./bun/preload": {
24
+ "types": "./dist/bun/preload.d.ts",
25
+ "bun": "./src/bun/preload.ts",
26
+ "development": "./src/bun/preload.ts",
27
+ "import": "./dist/bun/preload.js",
28
+ "default": "./dist/bun/preload.js"
29
+ },
23
30
  "./monorepo/runtime": {
24
31
  "types": "./dist/monorepo/runtime.d.ts",
25
32
  "bun": "./src/monorepo/runtime.ts",
@@ -42,6 +49,10 @@
42
49
  "default": "./dist/wrangler/prepare-env.js"
43
50
  }
44
51
  },
52
+ "sideEffects": [
53
+ "./dist/bun/preload.js",
54
+ "./src/bun/preload.ts"
55
+ ],
45
56
  "files": [
46
57
  "README.md",
47
58
  "bin",
@@ -53,8 +64,8 @@
53
64
  "dependencies": {
54
65
  "@arethetypeswrong/core": "^0.18.2",
55
66
  "@prettier/sync": "^0.6.1",
56
- "@smoothbricks/nx-plugin": "0.3.2",
57
- "@smoothbricks/validation": "0.1.3",
67
+ "@smoothbricks/nx-plugin": "0.3.4",
68
+ "@smoothbricks/validation": "0.1.4",
58
69
  "commander": "^14.0.3",
59
70
  "prettier": "^3.6.1",
60
71
  "publint": "^0.3.18",
@@ -62,7 +73,7 @@
62
73
  "sherif": "^1.11.1",
63
74
  "toml-eslint-parser": "^1.0.3",
64
75
  "tslib": "^2.8.1",
65
- "typia": "13.1.1"
76
+ "typia": "13.1.19"
66
77
  },
67
78
  "devDependencies": {
68
79
  "fast-check": "^4.4.0"
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bun runtime preload for consumers of `@smoothbricks/cli`.
3
+ *
4
+ * Re-exports the Typia/ttsc transform preload from `@smoothbricks/validation`.
5
+ * External repos depend on the CLI package only — they must not need a direct
6
+ * `@smoothbricks/validation` dependency just to run `smoo` against source.
7
+ *
8
+ * Usage:
9
+ * import '@smoothbricks/cli/bun/preload';
10
+ * // or bunfig.toml: preload = ["@smoothbricks/cli/bun/preload"]
11
+ */
12
+ import '@smoothbricks/validation/bun/preload';
package/src/cli.ts CHANGED
@@ -59,12 +59,15 @@ function buildProgram(): Command {
59
59
  );
60
60
  monorepo.command('update').action(async () => {
61
61
  const { updateManagedFiles } = await import('./monorepo/index.js');
62
- updateManagedFiles(await findRepoRoot());
63
- });
64
- monorepo.command('check').action(async () => {
65
- const { checkManagedFiles } = await import('./monorepo/index.js');
66
- checkManagedFiles(await findRepoRoot());
62
+ await updateManagedFiles(await findRepoRoot());
67
63
  });
64
+ monorepo
65
+ .command('check')
66
+ .option('--warn', 'report drift as warnings (GitHub annotations) instead of failing')
67
+ .action(async (options: { warn?: boolean }) => {
68
+ const { checkManagedFiles } = await import('./monorepo/index.js');
69
+ checkManagedFiles(await findRepoRoot(), { warn: options.warn });
70
+ });
68
71
  monorepo.command('diff').action(async () => {
69
72
  const { diffManagedFiles } = await import('./monorepo/index.js');
70
73
  diffManagedFiles(await findRepoRoot());
@@ -78,10 +81,19 @@ function buildProgram(): Command {
78
81
  });
79
82
  monorepo
80
83
  .command('sync-bun-lockfile-versions')
81
- .option('--stage', 'stage bun.lock when versions were resynced (pre-commit self-heal); quiet when clean')
82
- .action(async (options: { stage?: boolean }) => {
84
+ .option('--stage', 'stage bun.lock when versions were resynced; quiet when clean')
85
+ .option(
86
+ '--mode <mode>',
87
+ 'install: match package.json (default, CI); publish: map unpublished -next to last stable tag (pre-pack only)',
88
+ 'install',
89
+ )
90
+ .action(async (options: { stage?: boolean; mode?: 'install' | 'publish' }) => {
83
91
  const { syncBunLockfileVersions } = await import('./monorepo/index.js');
84
- syncBunLockfileVersions(await findRepoRoot(), options.stage ? { log: false, stage: true } : {});
92
+ const mode = options.mode === 'publish' ? 'publish' : 'install';
93
+ syncBunLockfileVersions(await findRepoRoot(), {
94
+ mode,
95
+ ...(options.stage ? { log: false, stage: true } : {}),
96
+ });
85
97
  });
86
98
  monorepo
87
99
  .command('list-release-packages')
@@ -42,12 +42,6 @@ export const variants: Record<string, GenerateVariant> = {
42
42
  args: (name) => ['--name', name, '--variant', 'ts-lib'],
43
43
  options: [{ flag: '--public', description: 'configure for npm publication' }],
44
44
  },
45
- 'ts-zig': {
46
- generator: 'create-package',
47
- description: 'Create a TypeScript + Zig/WASM hybrid package',
48
- args: (name) => ['--name', name, '--variant', 'ts-zig'],
49
- options: [{ flag: '--public', description: 'configure for npm publication' }],
50
- },
51
45
  'make-public': {
52
46
  generator: 'make-public',
53
47
  description: 'Promote a private package to npm publication',
@@ -1,11 +1,30 @@
1
- import { existsSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { appendFile, mkdtemp, realpath, rename, rm } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { $ } from 'bun';
5
+ import typia from 'typia';
6
+ import { parseStringArrayText } from '../lib/json.js';
5
7
  import { decode, run, runStatus } from '../lib/run.js';
6
8
  import { type ProjectTargets, readProjectTargets } from '../nx/index.js';
7
9
  import type { NxTargetRun } from './outputs.js';
8
10
 
11
+ interface GithubActionsEventPayload {
12
+ repository?: {
13
+ default_branch?: string;
14
+ };
15
+ }
16
+
17
+ interface NxProjectDeployConfigurations {
18
+ targets?: {
19
+ deploy?: {
20
+ configurations?: Record<string, unknown>;
21
+ };
22
+ };
23
+ }
24
+
25
+ const parseGithubActionsEvent = typia.json.createIsParse<GithubActionsEventPayload>();
26
+ const parseNxProjectDeployConfigurations = typia.json.createIsParse<NxProjectDeployConfigurations>();
27
+
9
28
  type NxSmartMode = 'auto' | 'affected' | 'run-many';
10
29
 
11
30
  export async function cleanupGithubCiCache(root: string): Promise<void> {
@@ -279,7 +298,7 @@ export async function githubCiApplyOutputs(
279
298
  async function loadOutputBoundary() {
280
299
  if (import.meta.url.endsWith('/src/github-ci/index.ts')) {
281
300
  // The source self-hosting shim has no Typia transform; register it before loading manifest validators.
282
- await import('@smoothbricks/validation/bun/preload');
301
+ await import('@smoothbricks/cli/bun/preload');
283
302
  }
284
303
  // This boundary must stay lazy because the transformed dist and source self-hosting paths initialize Typia differently.
285
304
  return import('./outputs.js');
@@ -341,7 +360,31 @@ function resolveNxSmartMode(mode: NxSmartMode): 'affected' | 'run-many' {
341
360
  if (mode === 'affected' || mode === 'run-many') {
342
361
  return mode;
343
362
  }
344
- return process.env.GITHUB_EVENT_NAME === 'push' && process.env.GITHUB_REF_NAME === 'main' ? 'run-many' : 'affected';
363
+ const defaultBranch = eventDefaultBranch() ?? 'main';
364
+ if (process.env.GITHUB_EVENT_NAME === 'push') {
365
+ return process.env.GITHUB_REF_NAME === defaultBranch ? 'run-many' : 'affected';
366
+ }
367
+ // A PR into a NON-default branch is an integration surface (e.g. a mirror-sync
368
+ // review branch): its base moves outside the default-branch workflow that
369
+ // affected scoping is calibrated against, so under-selection can pass PR CI
370
+ // and only fail after merge. Validate those PRs in full.
371
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
372
+ const base = process.env.GITHUB_BASE_REF;
373
+ return base && base !== defaultBranch ? 'run-many' : 'affected';
374
+ }
375
+ return 'affected';
376
+ }
377
+
378
+ /** The repository default branch from the Actions event payload. */
379
+ function eventDefaultBranch(): string | undefined {
380
+ const eventPath = process.env.GITHUB_EVENT_PATH;
381
+ if (!eventPath) return undefined;
382
+ try {
383
+ const payload = parseGithubActionsEvent(readFileSync(eventPath, 'utf8'));
384
+ return payload?.repository?.default_branch || undefined;
385
+ } catch {
386
+ return undefined;
387
+ }
345
388
  }
346
389
 
347
390
  async function deployProjectsWithConfiguration(
@@ -366,24 +409,12 @@ async function deployProjectsWithConfiguration(
366
409
 
367
410
  async function deployTargetHasConfiguration(root: string, project: string, configuration: string): Promise<boolean> {
368
411
  const result = await $`nx show project ${project} --json`.cwd(root).quiet();
369
- const parsed: unknown = JSON.parse(decode(result.stdout));
370
- const targets = recordValue(parsed)?.targets;
371
- const deploy = recordValue(targets)?.deploy;
372
- const configurations = recordValue(deploy)?.configurations;
373
- return recordValue(configurations)?.[configuration] !== undefined;
412
+ const parsed = parseNxProjectDeployConfigurations(decode(result.stdout));
413
+ return parsed?.targets?.deploy?.configurations?.[configuration] !== undefined;
374
414
  }
375
415
 
376
416
  function nxProjectList(output: string): string[] {
377
- const parsed: unknown = JSON.parse(output);
378
- return Array.isArray(parsed) ? parsed.filter((project): project is string => typeof project === 'string') : [];
379
- }
380
-
381
- function recordValue(value: unknown): Record<string, unknown> | undefined {
382
- return isRecord(value) ? value : undefined;
383
- }
384
-
385
- function isRecord(value: unknown): value is Record<string, unknown> {
386
- return value !== null && typeof value === 'object' && !Array.isArray(value);
417
+ return parseStringArrayText(output) ?? [];
387
418
  }
388
419
 
389
420
  async function createGithubStatus(name: string, step: string): Promise<void> {
@@ -2,7 +2,6 @@ import { createHash } from 'node:crypto';
2
2
  import { createReadStream, type Dirent, type Stats } from 'node:fs';
3
3
  import { copyFile, lstat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { isAbsolute, relative, resolve, sep } from 'node:path';
5
- import { isRecord } from '@smoothbricks/validation';
6
5
  import typia from 'typia';
7
6
  import { type ProjectTargets, readProjectTargets } from '../nx/index.js';
8
7
 
@@ -492,7 +491,7 @@ function assertGitSha(value: string, description: string): void {
492
491
  }
493
492
 
494
493
  function isMissingPathError(error: unknown): boolean {
495
- return isRecord(error) && error.code === 'ENOENT';
494
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
496
495
  }
497
496
 
498
497
  function sha256File(path: string): Promise<string> {
@@ -1,12 +1,12 @@
1
1
  import { fileURLToPath } from 'node:url';
2
- import { readJsonObject, stringProperty } from './json.js';
2
+ import { readJsonObject } from './json.js';
3
3
 
4
4
  export const smoothBricksCodebasePackageName = '@smoothbricks/codebase';
5
5
  export const cliPackageVersion = readCliPackageVersion();
6
6
 
7
7
  function readCliPackageVersion(): string {
8
8
  const pkg = readJsonObject(fileURLToPath(new URL('../../package.json', import.meta.url)));
9
- const version = pkg ? stringProperty(pkg, 'version') : null;
9
+ const version = pkg?.version;
10
10
  if (!version) {
11
11
  throw new Error('Unable to read @smoothbricks/cli package version.');
12
12
  }