@smoothbricks/cli 0.10.6 → 0.10.8

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 (82) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +24 -1
  3. package/dist/github-ci/index.d.ts +44 -3
  4. package/dist/github-ci/index.d.ts.map +1 -1
  5. package/dist/github-ci/index.js +242 -36
  6. package/dist/lib/json.d.ts +1 -1
  7. package/dist/lib/json.d.ts.map +1 -1
  8. package/dist/lib/json.js +3 -18
  9. package/dist/lib/workspace.d.ts.map +1 -1
  10. package/dist/lib/workspace.js +1 -0
  11. package/dist/monorepo/ci-workflow.js +16 -6
  12. package/dist/monorepo/managed-files.d.ts +6 -22
  13. package/dist/monorepo/managed-files.d.ts.map +1 -1
  14. package/dist/monorepo/managed-files.js +31 -48
  15. package/dist/monorepo/package-policy.js +2 -2
  16. package/dist/monorepo/packs/index.d.ts.map +1 -1
  17. package/dist/monorepo/packs/index.js +2 -0
  18. package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
  19. package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
  20. package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
  21. package/dist/monorepo/publish-workflow.js +3 -3
  22. package/dist/monorepo/runtime.d.ts +2 -2
  23. package/dist/monorepo/runtime.d.ts.map +1 -1
  24. package/dist/monorepo/runtime.js +11 -16
  25. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  26. package/dist/monorepo/tool-validation.js +105 -28
  27. package/dist/nx/index.d.ts +7 -4
  28. package/dist/nx/index.d.ts.map +1 -1
  29. package/dist/nx/index.js +117 -56
  30. package/dist/playwright/index.d.ts +22 -0
  31. package/dist/playwright/index.d.ts.map +1 -0
  32. package/dist/playwright/index.js +44 -0
  33. package/dist/release/bootstrap-npm-packages.d.ts +3 -0
  34. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
  35. package/dist/release/bootstrap-npm-packages.js +21 -0
  36. package/dist/release/index.d.ts.map +1 -1
  37. package/dist/release/index.js +17 -0
  38. package/dist/wrangler/cloudflare.d.ts +85 -0
  39. package/dist/wrangler/cloudflare.d.ts.map +1 -0
  40. package/dist/wrangler/cloudflare.js +235 -0
  41. package/dist/wrangler/deploy-environment.d.ts +48 -0
  42. package/dist/wrangler/deploy-environment.d.ts.map +1 -0
  43. package/dist/wrangler/deploy-environment.js +383 -0
  44. package/dist/wrangler/environment.d.ts +58 -0
  45. package/dist/wrangler/environment.d.ts.map +1 -0
  46. package/dist/wrangler/environment.js +297 -0
  47. package/managed/raw/tooling/devenv +71 -0
  48. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
  49. package/package.json +11 -4
  50. package/src/cli.ts +33 -3
  51. package/src/github-ci/index.test.ts +221 -1
  52. package/src/github-ci/index.ts +303 -32
  53. package/src/lib/json.ts +1 -1
  54. package/src/lib/workspace.ts +1 -0
  55. package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
  56. package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
  57. package/src/monorepo/__tests__/publish-workflow.test.ts +12 -4
  58. package/src/monorepo/ci-workflow.ts +16 -6
  59. package/src/monorepo/managed-files.test.ts +87 -23
  60. package/src/monorepo/managed-files.ts +35 -69
  61. package/src/monorepo/package-policy.test.ts +31 -0
  62. package/src/monorepo/package-policy.ts +2 -2
  63. package/src/monorepo/packs/index.ts +2 -0
  64. package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
  65. package/src/monorepo/publish-workflow.ts +7 -2
  66. package/src/monorepo/runtime.test.ts +23 -17
  67. package/src/monorepo/runtime.ts +12 -17
  68. package/src/monorepo/tool-validation.test.ts +100 -8
  69. package/src/monorepo/tool-validation.ts +115 -31
  70. package/src/nx/index.test.ts +45 -14
  71. package/src/nx/index.ts +133 -64
  72. package/src/playwright/index.test.ts +90 -0
  73. package/src/playwright/index.ts +73 -0
  74. package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
  75. package/src/release/bootstrap-npm-packages.ts +34 -0
  76. package/src/release/index.ts +18 -0
  77. package/src/wrangler/cloudflare.ts +289 -0
  78. package/src/wrangler/deploy-environment.test.ts +354 -0
  79. package/src/wrangler/deploy-environment.ts +445 -0
  80. package/src/wrangler/environment.test.ts +173 -0
  81. package/src/wrangler/environment.ts +366 -0
  82. package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
@@ -0,0 +1,297 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
3
+ import typia from 'typia';
4
+ import { cloneEnvBlock } from './prepare-env.js';
5
+ const MAX_PULL_REQUEST_NUMBER = 999999999;
6
+ const ENVIRONMENT_PATTERN = /^(?:staging|production|pr[1-9][0-9]{0,8})$/;
7
+ export function pullRequestEnvironment(prNumber) {
8
+ if (!Number.isInteger(prNumber) || prNumber < 1 || prNumber > MAX_PULL_REQUEST_NUMBER) {
9
+ throw new Error(`Pull request number must be an integer from 1 through ${MAX_PULL_REQUEST_NUMBER}.`);
10
+ }
11
+ return `pr${prNumber}`;
12
+ }
13
+ export function parseEnvironmentToken(value) {
14
+ if (!ENVIRONMENT_PATTERN.test(value)) {
15
+ throw new Error('Environment must be exactly staging, production, or pr followed by an integer from 1 through 999999999.');
16
+ }
17
+ if (value === 'staging' || value === 'production')
18
+ return value;
19
+ return pullRequestEnvironment(Number(value.slice(2)));
20
+ }
21
+ export function isPullRequestEnvironment(environment) {
22
+ return environment.startsWith('pr');
23
+ }
24
+ export function environmentDomain(environment, zone) {
25
+ const token = parseEnvironmentToken(environment);
26
+ if (!zone || zone.startsWith('.') || zone.endsWith('.')) {
27
+ throw new Error('Zone must be a non-empty DNS name without leading or trailing dots.');
28
+ }
29
+ return token === 'production' ? zone : `${token}.${zone}`;
30
+ }
31
+ export function environmentResourceName(base, environment) {
32
+ const token = parseEnvironmentToken(environment);
33
+ if (!base) {
34
+ throw new Error('Resource base name must not be empty.');
35
+ }
36
+ return token === 'production' ? base : `${base}-${token}`;
37
+ }
38
+ export function hasExactEnvironmentSegment(value, environment) {
39
+ const escaped = environment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ return new RegExp(`(?:^|[-.])${escaped}(?=$|[-.])`).test(value);
41
+ }
42
+ const isWranglerRoot = (() => {
43
+ const _io0 = input => undefined === input.env || "object" === typeof input.env && null !== input.env && false === Array.isArray(input.env) && _io1(input.env);
44
+ const _io1 = input => Object.keys(input).every(key => {
45
+ const value = input[key];
46
+ if (undefined === value)
47
+ return true;
48
+ return undefined === value || "object" === typeof value && null !== value && false === Array.isArray(value) && _io2(value);
49
+ });
50
+ const _io2 = input => true && true && true && true && true && true && Object.keys(input).every(key => {
51
+ if (["name", "routes", "kv_namespaces", "r2_buckets", "ratelimits", "vars"].some(prop => key === prop))
52
+ return true;
53
+ const value = input[key];
54
+ if (undefined === value)
55
+ return true;
56
+ return true;
57
+ });
58
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
59
+ })();
60
+ const isWranglerEnvironment = (() => {
61
+ const _io0 = input => true && true && true && true && true && true && Object.keys(input).every(key => {
62
+ if (["name", "routes", "kv_namespaces", "r2_buckets", "ratelimits", "vars"].some(prop => key === prop))
63
+ return true;
64
+ const value = input[key];
65
+ if (undefined === value)
66
+ return true;
67
+ return true;
68
+ });
69
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
70
+ })();
71
+ const isUnknownRecord = (() => {
72
+ const _io0 = input => Object.keys(input).every(key => {
73
+ const value = input[key];
74
+ if (undefined === value)
75
+ return true;
76
+ return true;
77
+ });
78
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
79
+ })();
80
+ const isUnknownRows = (() => {
81
+ const _io0 = input => Object.keys(input).every(key => {
82
+ const value = input[key];
83
+ if (undefined === value)
84
+ return true;
85
+ return true;
86
+ });
87
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io0(elem));
88
+ })();
89
+ export function planConfiguredEnvironmentResources(toml, environment) {
90
+ const block = parseRoot(toml).env?.[environment];
91
+ if (!isWranglerEnvironment(block)) {
92
+ throw new Error(`Wrangler configuration must declare [env.${environment}].`);
93
+ }
94
+ const workerName = requiredString(block, 'name', `[env.${environment}]`);
95
+ return {
96
+ environment,
97
+ workerName,
98
+ kvNamespaces: readKvBindings(block.kv_namespaces),
99
+ r2Buckets: readRows(block.r2_buckets).map((row) => {
100
+ const binding = requiredString(row, 'binding', 'R2 binding');
101
+ return { binding, bucketName: requiredString(row, 'bucket_name', `R2 binding ${binding}`) };
102
+ }),
103
+ routes: readRows(block.routes).map((row) => ({
104
+ pattern: requiredString(row, 'pattern', 'route'),
105
+ ...(typeof row.zone_name === 'string' ? { zoneName: row.zone_name } : {}),
106
+ customDomain: row.custom_domain === true,
107
+ })),
108
+ };
109
+ }
110
+ function parseRoot(toml) {
111
+ const value = getStaticTOMLValue(parseTOML(toml));
112
+ if (!isWranglerRoot(value)) {
113
+ throw new Error('Wrangler configuration is not a valid TOML environment document.');
114
+ }
115
+ return value;
116
+ }
117
+ function stagingEnvironment(toml) {
118
+ const staging = parseRoot(toml).env?.staging;
119
+ if (!isWranglerEnvironment(staging)) {
120
+ throw new Error('Wrangler configuration must declare [env.staging].');
121
+ }
122
+ return staging;
123
+ }
124
+ function stagingWorkerName(staging) {
125
+ if (typeof staging.name !== 'string' || !staging.name.endsWith('-staging')) {
126
+ throw new Error('[env.staging].name must end with the exact suffix -staging.');
127
+ }
128
+ return { workerName: staging.name, workerBaseName: staging.name.slice(0, -'-staging'.length) };
129
+ }
130
+ export function planPullRequestResources(toml, environment, liveNamespaces) {
131
+ parseEnvironmentToken(environment);
132
+ const staging = stagingEnvironment(toml);
133
+ const { workerBaseName } = stagingWorkerName(staging);
134
+ const namespaceById = new Map(liveNamespaces.map((namespace) => [namespace.id, namespace]));
135
+ const kvNamespaces = readKvBindings(staging.kv_namespaces).map(({ binding, id }) => {
136
+ const stagingNamespace = namespaceById.get(id);
137
+ if (!stagingNamespace) {
138
+ throw new Error(`Staging KV binding ${binding} references namespace ${id}, which is absent from the account listing.`);
139
+ }
140
+ const title = replaceExactToken(stagingNamespace.title, 'staging', environment);
141
+ if (title === stagingNamespace.title) {
142
+ throw new Error(`Staging KV namespace title ${stagingNamespace.title} has no exact staging segment.`);
143
+ }
144
+ return { binding, stagingId: id, stagingTitle: stagingNamespace.title, title };
145
+ });
146
+ const r2Buckets = readRows(staging.r2_buckets).map((row) => {
147
+ const binding = requiredString(row, 'binding', 'R2 binding');
148
+ const stagingBucket = requiredString(row, 'bucket_name', `R2 binding ${binding}`);
149
+ const bucketName = replaceExactToken(stagingBucket, 'staging', environment);
150
+ if (bucketName === stagingBucket) {
151
+ throw new Error(`Staging R2 bucket ${stagingBucket} has no exact staging segment.`);
152
+ }
153
+ return { binding, bucketName };
154
+ });
155
+ const routes = readRows(staging.routes).map((row) => ({
156
+ pattern: replaceHostnameLabel(requiredString(row, 'pattern', 'route'), environment),
157
+ ...(typeof row.zone_name === 'string' ? { zoneName: row.zone_name } : {}),
158
+ customDomain: row.custom_domain === true,
159
+ }));
160
+ return {
161
+ environment,
162
+ workerName: environmentResourceName(workerBaseName, environment),
163
+ workerBaseName,
164
+ kvNamespaces,
165
+ r2Buckets,
166
+ routes,
167
+ };
168
+ }
169
+ export function derivePullRequestWranglerConfig(toml, options) {
170
+ const environment = options.environment;
171
+ parseEnvironmentToken(environment);
172
+ if (!options.accountId) {
173
+ throw new Error('Cloudflare account id is required to derive rate-limit namespaces.');
174
+ }
175
+ const staging = stagingEnvironment(toml);
176
+ const { workerBaseName } = stagingWorkerName(staging);
177
+ const cloned = cloneEnvBlock(toml, 'staging', environment);
178
+ const program = parseTOML(cloned);
179
+ const rootValue = getStaticTOMLValue(program);
180
+ if (!isWranglerRoot(rootValue)) {
181
+ throw new Error('Derived Wrangler configuration is not a valid environment document.');
182
+ }
183
+ const root = rootValue;
184
+ const edits = [];
185
+ for (const table of program.body[0].body) {
186
+ if (table.type !== 'TOMLTable' || table.resolvedKey[0] !== 'env' || table.resolvedKey[1] !== environment) {
187
+ continue;
188
+ }
189
+ const tableValue = valueAtPath(root, table.resolvedKey);
190
+ if (!isUnknownRecord(tableValue)) {
191
+ continue;
192
+ }
193
+ for (const keyValue of table.body) {
194
+ const key = cloned.slice(keyValue.key.range[0], keyValue.key.range[1]).trim();
195
+ const current = tableValue[key];
196
+ const next = deriveFieldValue(table.resolvedKey.slice(2), tableValue, key, current, environment, workerBaseName, options.accountId, options.kvNamespaceIds);
197
+ if (next !== current) {
198
+ edits.push({ start: keyValue.value.range[0], end: keyValue.value.range[1], value: tomlLiteral(next) });
199
+ }
200
+ }
201
+ }
202
+ let derived = cloned;
203
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
204
+ derived = derived.slice(0, edit.start) + edit.value + derived.slice(edit.end);
205
+ }
206
+ parseTOML(derived);
207
+ return derived;
208
+ }
209
+ function deriveFieldValue(path, table, key, current, environment, workerBaseName, accountId, kvNamespaceIds) {
210
+ const section = path[0];
211
+ if (path.length === 0 && key === 'name') {
212
+ return environmentResourceName(workerBaseName, environment);
213
+ }
214
+ if (section === 'routes' && key === 'pattern' && typeof current === 'string') {
215
+ return replaceHostnameLabel(current, environment);
216
+ }
217
+ if (section === 'vars' && typeof current === 'string') {
218
+ if (key === 'ENVIRONMENT') {
219
+ return environment;
220
+ }
221
+ if (key === 'AUTH_KEYS_INSTANCE_NAME') {
222
+ return replaceExactToken(current, 'staging', environment);
223
+ }
224
+ return replaceHostnameLabel(current, environment);
225
+ }
226
+ if (section === 'send_email' && key === 'allowed_sender_addresses' && Array.isArray(current)) {
227
+ return current.map((value) => (typeof value === 'string' ? replaceHostnameLabel(value, environment) : value));
228
+ }
229
+ if (section === 'kv_namespaces' && key === 'id' && typeof current === 'string') {
230
+ const derived = kvNamespaceIds.get(current);
231
+ if (!derived) {
232
+ throw new Error(`No derived KV namespace id was supplied for staging namespace ${current}.`);
233
+ }
234
+ return derived;
235
+ }
236
+ if (section === 'r2_buckets' && key === 'bucket_name' && typeof current === 'string') {
237
+ return replaceExactToken(current, 'staging', environment);
238
+ }
239
+ if (section === 'ratelimits' && key === 'namespace_id') {
240
+ const bindingName = requiredString(table, 'name', 'Rate-limit binding');
241
+ return rateLimitNamespaceId(accountId, workerBaseName, environment, bindingName);
242
+ }
243
+ return current;
244
+ }
245
+ export function rateLimitNamespaceId(accountId, workerBaseName, environment, bindingName) {
246
+ const token = parseEnvironmentToken(environment);
247
+ const digest = createHash('sha256').update(`${accountId}:${workerBaseName}:${token}:${bindingName}`).digest();
248
+ const value = digest.readUInt32BE(0) & 2147483647;
249
+ return String(value === 0 ? 1 : value);
250
+ }
251
+ function replaceHostnameLabel(value, environment) {
252
+ return value.replace(/(^|[.@/])staging(?=\.)/g, `$1${environment}`);
253
+ }
254
+ function replaceExactToken(value, from, to) {
255
+ const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
256
+ return value.replace(new RegExp(`(^|[-.])${escaped}(?=$|[-.])`, 'g'), `$1${to}`);
257
+ }
258
+ function readKvBindings(value) {
259
+ return readRows(value).map((row) => ({
260
+ binding: requiredString(row, 'binding', 'KV namespace'),
261
+ id: requiredString(row, 'id', 'KV namespace'),
262
+ }));
263
+ }
264
+ function readRows(value) {
265
+ return isUnknownRows(value) ? value : [];
266
+ }
267
+ function requiredString(row, key, context) {
268
+ const value = row[key];
269
+ if (typeof value !== 'string' || !value) {
270
+ throw new Error(`${context} must declare a non-empty ${key}.`);
271
+ }
272
+ return value;
273
+ }
274
+ function valueAtPath(root, path) {
275
+ let value = root;
276
+ for (const segment of path) {
277
+ if (typeof segment === 'number') {
278
+ if (!Array.isArray(value))
279
+ return undefined;
280
+ value = value[segment];
281
+ }
282
+ else {
283
+ if (!isUnknownRecord(value))
284
+ return undefined;
285
+ value = value[segment];
286
+ }
287
+ }
288
+ return value;
289
+ }
290
+ function tomlLiteral(value) {
291
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) {
292
+ const literal = JSON.stringify(value);
293
+ if (literal !== undefined)
294
+ return literal;
295
+ }
296
+ throw new Error(`Cannot materialize Wrangler TOML value of type ${typeof value}.`);
297
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper script to run devenv from anywhere in the monorepo
3
+ # This allows us to keep all devenv files in tooling/direnv
4
+
5
+ # Save the current working directory
6
+ ORIGINAL_PWD="$(pwd)"
7
+ SELF_PATH="$(realpath "$0")"
8
+
9
+ # Find the root of the git repository
10
+ TOOLING_DIR="$(dirname "$SELF_PATH")"
11
+ DEVENV_DIR="$TOOLING_DIR/direnv"
12
+ if [ ! -d "$DEVENV_DIR" ]; then
13
+ echo "Error: Could not find devenv directory at $DEVENV_DIR" >&2
14
+ exit 1
15
+ fi
16
+
17
+ # Find the actual Nix-built devenv binary, skipping every wrapper script.
18
+ # WHY: PATH routinely holds more than one of these wrappers at once — this
19
+ # repo's tooling/devenv, sibling repos' wrappers, and direnv-stacked parent
20
+ # environments. A plain "not equal to SELF_PATH" check happily selects a
21
+ # *different* wrapper and exec's it, so `devenv` bounces into the wrong repo
22
+ # (or self-execs forever on cheap checks like `devenv version`). The real devenv
23
+ # is always the Nix-managed executable under /nix/store, so match that and ignore
24
+ # every wrapper regardless of PATH ordering. (Matching the store path is also more
25
+ # robust than a "is it a binary" test: Nix sometimes ships tools as makeWrapper
26
+ # shell scripts, which are still the correct entry point.)
27
+ DEVENV_BIN=""
28
+ while IFS= read -r candidate; do
29
+ candidate_path="$(realpath "$candidate")"
30
+ case "$candidate_path" in
31
+ /nix/store/*)
32
+ if [ -f "$candidate_path" ] && [ -x "$candidate_path" ]; then
33
+ DEVENV_BIN="$candidate_path"
34
+ break
35
+ fi
36
+ ;;
37
+ esac
38
+ done < <(which -a devenv 2>/dev/null)
39
+
40
+ if [ -z "$DEVENV_BIN" ]; then
41
+ echo "Error: Could not find the Nix-built devenv binary (under /nix/store) in PATH" >&2
42
+ exit 1
43
+ fi
44
+
45
+ # If we're in a project, just run devenv
46
+ if [ -f devenv.nix ]; then
47
+ exec "$DEVENV_BIN" "$@"
48
+ fi
49
+
50
+ # Change to the tooling/direnv directory where devenv files are located
51
+ cd "$DEVENV_DIR" || exit 1
52
+
53
+ # Export the original directory so enterShell can use it
54
+ export DEVENV_SHELL_PWD="$ORIGINAL_PWD"
55
+
56
+ # Run the actual devenv command with all arguments passed through.
57
+ # For updates, refresh nvfetcher-managed overlay sources first so packages like
58
+ # jmunch-mcp and jcodemunch-mcp are current before devenv.lock is updated.
59
+ if [ "${1:-}" = "update" ]; then
60
+ if [ -f "$DEVENV_DIR/nixpkgs-overlay/nvfetcher.toml" ]; then
61
+ echo "Running nvfetcher for nixpkgs-overlay/..."
62
+ (cd "$DEVENV_DIR/nixpkgs-overlay" && echo "
63
+ [keys]
64
+ github = \"$(gh auth token)\"
65
+ " > .gh-auth && (nvfetcher -k .gh-auth -o _sources || true) && rm .gh-auth)
66
+ fi
67
+
68
+ exec "$DEVENV_BIN" "$@"
69
+ fi
70
+
71
+ exec "$DEVENV_BIN" "$@"
@@ -4,15 +4,14 @@ set -euo pipefail
4
4
  NIX_STORE_NAR="${NIX_STORE_NAR:-/tmp/nix-store.nar}"
5
5
  nix_store_cmd="/nix/var/nix/profiles/default/bin/nix-store"
6
6
  DEVENV_FLAKE="${DEVENV_FLAKE:-github:cachix/devenv}"
7
- # Host CI cache (GARM bind-mount). Prefer env from runner profile.
8
- # Path comes from the runner (GARM profile). Ephemeral runners leave unset;
9
- # devenv enterShell defaults to $PWD/.cache/ttsc.
10
- TTSC_CACHE_DIR="${TTSC_CACHE_DIR:-}"
11
7
  # Resolve from this script's location, not the caller's cwd. GitHub Actions
12
8
  # runs this from tooling/direnv today, but direct cwd-changing helpers are easy
13
9
  # to misuse and break on repeated calls.
14
10
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
11
  repo_root="$(cd "$script_dir/../.." && pwd)"
12
+ # Host CI supplies its bind-mounted cache through the runner profile. Ephemeral
13
+ # runners must use the same repository path restored by cache-ttsc-plugins.
14
+ TTSC_CACHE_DIR="${TTSC_CACHE_DIR:-$repo_root/.cache/ttsc}"
16
15
 
17
16
  clear_devenv_cache_state() {
18
17
  rm -rf "$repo_root/tooling/direnv/.devenv" "$repo_root/tooling/direnv/.direnv"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -47,6 +47,13 @@
47
47
  "development": "./src/wrangler/prepare-env.ts",
48
48
  "import": "./dist/wrangler/prepare-env.js",
49
49
  "default": "./dist/wrangler/prepare-env.js"
50
+ },
51
+ "./wrangler/environment": {
52
+ "types": "./dist/wrangler/environment.d.ts",
53
+ "bun": "./src/wrangler/environment.ts",
54
+ "development": "./src/wrangler/environment.ts",
55
+ "import": "./dist/wrangler/environment.js",
56
+ "default": "./dist/wrangler/environment.js"
50
57
  }
51
58
  },
52
59
  "sideEffects": [
@@ -64,8 +71,8 @@
64
71
  "dependencies": {
65
72
  "@arethetypeswrong/core": "^0.18.2",
66
73
  "@prettier/sync": "^0.6.1",
67
- "@smoothbricks/nx-plugin": "0.3.4",
68
- "@smoothbricks/validation": "0.1.4",
74
+ "@smoothbricks/nx-plugin": "0.3.5",
75
+ "@smoothbricks/validation": "0.1.5",
69
76
  "commander": "^14.0.3",
70
77
  "prettier": "^3.6.1",
71
78
  "publint": "^0.3.18",
@@ -73,7 +80,7 @@
73
80
  "sherif": "^1.11.1",
74
81
  "toml-eslint-parser": "^1.0.3",
75
82
  "tslib": "^2.8.1",
76
- "typia": "13.1.19"
83
+ "typia": "13.2.0"
77
84
  },
78
85
  "devDependencies": {
79
86
  "fast-check": "^4.4.0"
package/src/cli.ts CHANGED
@@ -2,7 +2,9 @@ import { Command, CommanderError } from 'commander';
2
2
  import { variants } from './generate/index.js';
3
3
  import { cliPackageVersion } from './lib/cli-package.js';
4
4
  import { findRepoRoot } from './lib/run.js';
5
+ import { ensureChromium } from './playwright/index.js';
5
6
  import { resolvePrConflicts } from './pr/index.js';
7
+ import { cleanupPullRequest, deployEnvironment } from './wrangler/deploy-environment.js';
6
8
  import { scaffold } from './wrangler/scaffold.js';
7
9
 
8
10
  export async function runCli(argv = process.argv.slice(2)): Promise<void> {
@@ -348,10 +350,17 @@ function buildProgram(): Command {
348
350
  .command('nx-run-many')
349
351
  .requiredOption('--targets <targets>')
350
352
  .option('--projects <projects>')
353
+ .option('--projects-with-targets <targets>', 'select projects owning any comma-separated target or target glob')
351
354
  .option('--configuration <configuration>')
352
355
  .option('--collect-outputs <directory>')
353
356
  .action(
354
- async (options: { targets: string; projects?: string; configuration?: string; collectOutputs?: string }) => {
357
+ async (options: {
358
+ targets: string;
359
+ projects?: string;
360
+ projectsWithTargets?: string;
361
+ configuration?: string;
362
+ collectOutputs?: string;
363
+ }) => {
355
364
  const { githubCiNxRunMany } = await import('./github-ci/index.js');
356
365
  await githubCiNxRunMany(await findRepoRoot(), options);
357
366
  },
@@ -366,14 +375,14 @@ function buildProgram(): Command {
366
375
  });
367
376
  githubCi
368
377
  .command('nx-deploy')
369
- .requiredOption('--configuration <configuration>')
378
+ .option('--environment <environment>', 'explicit staging, production, or prN override')
370
379
  .option('--mode <mode>', 'auto, affected, or run-many', 'run-many')
371
380
  .option('--name <name>')
372
381
  .option('--step <step>')
373
382
  .option('--verify', 'run build, lint, and test before deploy')
374
383
  .action(
375
384
  async (options: {
376
- configuration: string;
385
+ environment?: string;
377
386
  mode?: 'auto' | 'affected' | 'run-many';
378
387
  name?: string;
379
388
  step?: string;
@@ -396,6 +405,15 @@ function buildProgram(): Command {
396
405
  }
397
406
  });
398
407
 
408
+ const playwright = program.command('playwright').description('Manage Playwright browsers');
409
+ const playwrightEnsure = playwright.command('ensure').description('Ensure a Playwright browser is available');
410
+ playwrightEnsure
411
+ .command('chromium')
412
+ .description('Ensure Chromium is available for browser tests')
413
+ .action(async () => {
414
+ await ensureChromium();
415
+ });
416
+
399
417
  const wrangler = program.command('wrangler').description('Cloudflare wrangler project helpers');
400
418
  wrangler
401
419
  .command('scaffold <project>')
@@ -404,6 +422,18 @@ function buildProgram(): Command {
404
422
  .action(async (project: string, options: { force?: boolean }) => {
405
423
  scaffold(await findRepoRoot(), project, { force: options.force });
406
424
  });
425
+ wrangler
426
+ .command('deploy-environment')
427
+ .requiredOption('--environment <environment>', 'staging, production, or prN')
428
+ .action(async (options: { environment: string }) => {
429
+ await deployEnvironment(process.cwd(), options.environment);
430
+ });
431
+ wrangler
432
+ .command('cleanup-pr')
433
+ .requiredOption('--pr <number>', 'pull-request number')
434
+ .action(async (options: { pr: string }) => {
435
+ await cleanupPullRequest(process.cwd(), Number(options.pr));
436
+ });
407
437
 
408
438
  return program;
409
439
  }