@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +24 -1
- package/dist/github-ci/index.d.ts +44 -3
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +242 -36
- package/dist/lib/json.d.ts +1 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +3 -18
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +1 -0
- package/dist/monorepo/ci-workflow.js +16 -6
- package/dist/monorepo/managed-files.d.ts +6 -22
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +31 -48
- package/dist/monorepo/package-policy.js +2 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +2 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
- package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
- package/dist/monorepo/publish-workflow.js +3 -3
- package/dist/monorepo/runtime.d.ts +2 -2
- package/dist/monorepo/runtime.d.ts.map +1 -1
- package/dist/monorepo/runtime.js +11 -16
- package/dist/monorepo/tool-validation.d.ts.map +1 -1
- package/dist/monorepo/tool-validation.js +105 -28
- package/dist/nx/index.d.ts +7 -4
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +117 -56
- package/dist/playwright/index.d.ts +22 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +44 -0
- package/dist/release/bootstrap-npm-packages.d.ts +3 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
- package/dist/release/bootstrap-npm-packages.js +21 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +17 -0
- package/dist/wrangler/cloudflare.d.ts +85 -0
- package/dist/wrangler/cloudflare.d.ts.map +1 -0
- package/dist/wrangler/cloudflare.js +235 -0
- package/dist/wrangler/deploy-environment.d.ts +48 -0
- package/dist/wrangler/deploy-environment.d.ts.map +1 -0
- package/dist/wrangler/deploy-environment.js +383 -0
- package/dist/wrangler/environment.d.ts +58 -0
- package/dist/wrangler/environment.d.ts.map +1 -0
- package/dist/wrangler/environment.js +297 -0
- package/managed/raw/tooling/devenv +71 -0
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
- package/package.json +11 -4
- package/src/cli.ts +33 -3
- package/src/github-ci/index.test.ts +221 -1
- package/src/github-ci/index.ts +303 -32
- package/src/lib/json.ts +1 -1
- package/src/lib/workspace.ts +1 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
- package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +12 -4
- package/src/monorepo/ci-workflow.ts +16 -6
- package/src/monorepo/managed-files.test.ts +87 -23
- package/src/monorepo/managed-files.ts +35 -69
- package/src/monorepo/package-policy.test.ts +31 -0
- package/src/monorepo/package-policy.ts +2 -2
- package/src/monorepo/packs/index.ts +2 -0
- package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
- package/src/monorepo/publish-workflow.ts +7 -2
- package/src/monorepo/runtime.test.ts +23 -17
- package/src/monorepo/runtime.ts +12 -17
- package/src/monorepo/tool-validation.test.ts +100 -8
- package/src/monorepo/tool-validation.ts +115 -31
- package/src/nx/index.test.ts +45 -14
- package/src/nx/index.ts +133 -64
- package/src/playwright/index.test.ts +90 -0
- package/src/playwright/index.ts +73 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
- package/src/release/bootstrap-npm-packages.ts +34 -0
- package/src/release/index.ts +18 -0
- package/src/wrangler/cloudflare.ts +289 -0
- package/src/wrangler/deploy-environment.test.ts +354 -0
- package/src/wrangler/deploy-environment.ts +445 -0
- package/src/wrangler/environment.test.ts +173 -0
- package/src/wrangler/environment.ts +366 -0
- 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.
|
|
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.
|
|
68
|
-
"@smoothbricks/validation": "0.1.
|
|
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.
|
|
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: {
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
}
|