@smoothbricks/cli 0.10.7 → 0.10.9

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 (58) 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 -4
  4. package/dist/github-ci/index.d.ts.map +1 -1
  5. package/dist/github-ci/index.js +220 -34
  6. package/dist/monorepo/ci-workflow.js +16 -6
  7. package/dist/monorepo/managed-files.d.ts.map +1 -1
  8. package/dist/monorepo/managed-files.js +19 -1
  9. package/dist/monorepo/pr-preview-cleanup-workflow.d.ts +5 -0
  10. package/dist/monorepo/pr-preview-cleanup-workflow.d.ts.map +1 -0
  11. package/dist/monorepo/pr-preview-cleanup-workflow.js +38 -0
  12. package/dist/monorepo/publish-workflow.js +3 -3
  13. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  14. package/dist/monorepo/tool-validation.js +85 -5
  15. package/dist/playwright/index.d.ts +22 -0
  16. package/dist/playwright/index.d.ts.map +1 -0
  17. package/dist/playwright/index.js +44 -0
  18. package/dist/release/bootstrap-npm-packages.d.ts +3 -0
  19. package/dist/release/bootstrap-npm-packages.d.ts.map +1 -1
  20. package/dist/release/bootstrap-npm-packages.js +21 -0
  21. package/dist/release/index.d.ts +1 -0
  22. package/dist/release/index.d.ts.map +1 -1
  23. package/dist/release/index.js +31 -6
  24. package/dist/wrangler/cloudflare.d.ts +87 -0
  25. package/dist/wrangler/cloudflare.d.ts.map +1 -0
  26. package/dist/wrangler/cloudflare.js +238 -0
  27. package/dist/wrangler/deploy-environment.d.ts +48 -0
  28. package/dist/wrangler/deploy-environment.d.ts.map +1 -0
  29. package/dist/wrangler/deploy-environment.js +383 -0
  30. package/dist/wrangler/environment.d.ts +58 -0
  31. package/dist/wrangler/environment.d.ts.map +1 -0
  32. package/dist/wrangler/environment.js +297 -0
  33. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +3 -4
  34. package/package.json +9 -2
  35. package/src/cli.ts +27 -3
  36. package/src/github-ci/index.test.ts +175 -2
  37. package/src/github-ci/index.ts +274 -31
  38. package/src/monorepo/__tests__/ci-workflow.test.ts +10 -4
  39. package/src/monorepo/__tests__/pr-preview-cleanup-workflow.test.ts +23 -0
  40. package/src/monorepo/__tests__/publish-workflow.test.ts +7 -5
  41. package/src/monorepo/ci-workflow.ts +16 -6
  42. package/src/monorepo/managed-files.test.ts +56 -1
  43. package/src/monorepo/managed-files.ts +20 -1
  44. package/src/monorepo/pr-preview-cleanup-workflow.ts +44 -0
  45. package/src/monorepo/publish-workflow.ts +8 -5
  46. package/src/monorepo/tool-validation.test.ts +85 -0
  47. package/src/monorepo/tool-validation.ts +94 -5
  48. package/src/playwright/index.test.ts +90 -0
  49. package/src/playwright/index.ts +73 -0
  50. package/src/release/__tests__/bootstrap-npm-packages.test.ts +63 -2
  51. package/src/release/bootstrap-npm-packages.ts +34 -0
  52. package/src/release/index.ts +30 -4
  53. package/src/wrangler/cloudflare.test.ts +76 -0
  54. package/src/wrangler/cloudflare.ts +292 -0
  55. package/src/wrangler/deploy-environment.test.ts +354 -0
  56. package/src/wrangler/deploy-environment.ts +445 -0
  57. package/src/wrangler/environment.test.ts +173 -0
  58. package/src/wrangler/environment.ts +366 -0
@@ -0,0 +1,238 @@
1
+ import typia from 'typia';
2
+ const isCloudflareEnvelope = (() => {
3
+ const _ip0 = input => undefined === input["code"] || "number" === typeof input["code"];
4
+ const _ip1 = input => undefined === input["message"] || "string" === typeof input["message"];
5
+ const _io0 = input => "boolean" === typeof input.success && true && (null === input.errors || undefined === input.errors || Array.isArray(input.errors) && input.errors.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io1(elem))) && (null === input.messages || undefined === input.messages || Array.isArray(input.messages) && input.messages.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io2(elem))) && (undefined === input.result_info || "object" === typeof input.result_info && null !== input.result_info && false === Array.isArray(input.result_info) && _io3(input.result_info));
6
+ const _io1 = input => _ip0(input) && _ip1(input);
7
+ const _io2 = input => _ip0(input) && _ip1(input);
8
+ const _io3 = input => (undefined === input.page || "number" === typeof input.page) && (undefined === input.total_pages || "number" === typeof input.total_pages) && (undefined === input.cursor || "string" === typeof input.cursor) && (undefined === input.is_truncated || "boolean" === typeof input.is_truncated);
9
+ return input => "object" === typeof input && null !== input && _io0(input);
10
+ })();
11
+ const isKvNamespaces = (() => {
12
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.title;
13
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
14
+ })();
15
+ const isR2Buckets = (() => {
16
+ const _io0 = input => "string" === typeof input.name;
17
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
18
+ })();
19
+ const isWorkerScripts = (() => {
20
+ const _io0 = input => "string" === typeof input.id;
21
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
22
+ })();
23
+ const isWorkerDomains = (() => {
24
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.hostname && (undefined === input.service || "string" === typeof input.service);
25
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
26
+ })();
27
+ const isCloudflareZones = (() => {
28
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.name;
29
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
30
+ })();
31
+ const isWorkerRoutes = (() => {
32
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.pattern && (undefined === input.script || "string" === typeof input.script);
33
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
34
+ })();
35
+ const isDnsRecords = (() => {
36
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.name && "string" === typeof input.type && "string" === typeof input.content && (undefined === input.proxied || "boolean" === typeof input.proxied);
37
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
38
+ })();
39
+ const isR2Objects = (() => {
40
+ const _io0 = input => "string" === typeof input.key;
41
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
42
+ })();
43
+ const isR2BucketPage = (() => {
44
+ const _io0 = input => Array.isArray(input.buckets) && input.buckets.every(elem => "object" === typeof elem && null !== elem && _io1(elem));
45
+ const _io1 = input => "string" === typeof input.name;
46
+ return input => "object" === typeof input && null !== input && _io0(input);
47
+ })();
48
+ const isR2ObjectPage = (() => {
49
+ const _io0 = input => Array.isArray(input.objects) && input.objects.every(elem => "object" === typeof elem && null !== elem && _io1(elem));
50
+ const _io1 = input => "string" === typeof input.key;
51
+ return input => "object" === typeof input && null !== input && _io0(input);
52
+ })();
53
+ const isCreatedKvNamespace = (() => {
54
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.title;
55
+ return input => "object" === typeof input && null !== input && _io0(input);
56
+ })();
57
+ export class CloudflareApiError extends Error {
58
+ status;
59
+ codes;
60
+ constructor(message, status, codes) {
61
+ super(message);
62
+ this.status = status;
63
+ this.codes = codes;
64
+ }
65
+ }
66
+ export class CloudflareRestClient {
67
+ apiToken;
68
+ fetcher;
69
+ accountPath;
70
+ constructor(accountId, apiToken, fetcher = fetch) {
71
+ this.apiToken = apiToken;
72
+ this.fetcher = fetcher;
73
+ if (!accountId || !apiToken) {
74
+ throw new Error('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required.');
75
+ }
76
+ this.accountPath = `/accounts/${encodeURIComponent(accountId)}`;
77
+ }
78
+ listKvNamespaces() {
79
+ return this.listPageItems(`${this.accountPath}/storage/kv/namespaces`, isKvNamespaces);
80
+ }
81
+ async createKvNamespace(title) {
82
+ const result = await this.result(`${this.accountPath}/storage/kv/namespaces`, {
83
+ method: 'POST',
84
+ body: JSON.stringify({ title }),
85
+ });
86
+ if (!isCreatedKvNamespace(result)) {
87
+ throw new Error(`Cloudflare returned an invalid KV namespace after creating ${title}.`);
88
+ }
89
+ return result;
90
+ }
91
+ async deleteKvNamespace(id) {
92
+ await this.result(`${this.accountPath}/storage/kv/namespaces/${encodeURIComponent(id)}`, { method: 'DELETE' });
93
+ }
94
+ async listR2Buckets() {
95
+ const buckets = [];
96
+ let page = 1;
97
+ for (;;) {
98
+ const envelope = await this.request(`${this.accountPath}/r2/buckets?per_page=1000&page=${page}`);
99
+ let rows;
100
+ if (isR2Buckets(envelope.result))
101
+ rows = envelope.result;
102
+ else if (isR2BucketPage(envelope.result))
103
+ rows = envelope.result.buckets;
104
+ else
105
+ throw new Error('Cloudflare returned an invalid R2 bucket listing.');
106
+ buckets.push(...rows);
107
+ const totalPages = envelope.result_info?.total_pages ?? page;
108
+ if (page >= totalPages)
109
+ break;
110
+ page += 1;
111
+ }
112
+ return buckets;
113
+ }
114
+ async createR2Bucket(name) {
115
+ await this.result(`${this.accountPath}/r2/buckets`, { method: 'POST', body: JSON.stringify({ name }) });
116
+ }
117
+ async listR2Objects(bucket) {
118
+ const keys = [];
119
+ let cursor;
120
+ do {
121
+ const query = new URLSearchParams({ per_page: '1000' });
122
+ if (cursor)
123
+ query.set('cursor', cursor);
124
+ const envelope = await this.request(`${this.accountPath}/r2/buckets/${encodeURIComponent(bucket)}/objects?${query.toString()}`);
125
+ let rows;
126
+ if (isR2Objects(envelope.result))
127
+ rows = envelope.result;
128
+ else if (isR2ObjectPage(envelope.result))
129
+ rows = envelope.result.objects;
130
+ else
131
+ throw new Error(`Cloudflare returned an invalid object listing for R2 bucket ${bucket}.`);
132
+ keys.push(...rows.map((row) => row.key));
133
+ cursor = envelope.result_info?.is_truncated === true ? envelope.result_info.cursor : undefined;
134
+ } while (cursor);
135
+ return keys;
136
+ }
137
+ async deleteR2Object(bucket, key) {
138
+ const objectPath = key.split('/').map(encodeURIComponent).join('/');
139
+ await this.result(`${this.accountPath}/r2/buckets/${encodeURIComponent(bucket)}/objects/${objectPath}`, {
140
+ method: 'DELETE',
141
+ });
142
+ }
143
+ async deleteR2Bucket(name) {
144
+ await this.result(`${this.accountPath}/r2/buckets/${encodeURIComponent(name)}`, { method: 'DELETE' });
145
+ }
146
+ listWorkerScripts() {
147
+ return this.listPageItems(`${this.accountPath}/workers/scripts`, isWorkerScripts);
148
+ }
149
+ async deleteWorkerScript(name) {
150
+ await this.result(`${this.accountPath}/workers/scripts/${encodeURIComponent(name)}`, { method: 'DELETE' });
151
+ }
152
+ listWorkerDomains() {
153
+ return this.listPageItems(`${this.accountPath}/workers/domains`, isWorkerDomains);
154
+ }
155
+ async createWorkerDomain(hostname, workerName, zoneId) {
156
+ await this.result(`${this.accountPath}/workers/domains`, {
157
+ method: 'PUT',
158
+ body: JSON.stringify({ hostname, service: workerName, zone_id: zoneId }),
159
+ });
160
+ }
161
+ async deleteWorkerDomain(id) {
162
+ await this.result(`${this.accountPath}/workers/domains/${encodeURIComponent(id)}`, { method: 'DELETE' });
163
+ }
164
+ listZones() {
165
+ return this.listPageItems('/zones', isCloudflareZones);
166
+ }
167
+ listWorkerRoutes(zoneId) {
168
+ return this.listPageItems(`/zones/${encodeURIComponent(zoneId)}/workers/routes`, isWorkerRoutes);
169
+ }
170
+ async createWorkerRoute(zoneId, pattern, workerName) {
171
+ await this.result(`/zones/${encodeURIComponent(zoneId)}/workers/routes`, {
172
+ method: 'POST',
173
+ body: JSON.stringify({ pattern, script: workerName }),
174
+ });
175
+ }
176
+ async deleteWorkerRoute(zoneId, routeId) {
177
+ await this.result(`/zones/${encodeURIComponent(zoneId)}/workers/routes/${encodeURIComponent(routeId)}`, {
178
+ method: 'DELETE',
179
+ });
180
+ }
181
+ listDnsRecords(zoneId) {
182
+ return this.listPageItems(`/zones/${encodeURIComponent(zoneId)}/dns_records`, isDnsRecords);
183
+ }
184
+ async createDnsRecord(zoneId, name, content) {
185
+ await this.result(`/zones/${encodeURIComponent(zoneId)}/dns_records`, {
186
+ method: 'POST',
187
+ body: JSON.stringify({ type: 'CNAME', name, content, proxied: true }),
188
+ });
189
+ }
190
+ async deleteDnsRecord(zoneId, recordId) {
191
+ await this.result(`/zones/${encodeURIComponent(zoneId)}/dns_records/${encodeURIComponent(recordId)}`, {
192
+ method: 'DELETE',
193
+ });
194
+ }
195
+ async listPageItems(path, isItems) {
196
+ const items = [];
197
+ let page = 1;
198
+ for (;;) {
199
+ const separator = path.includes('?') ? '&' : '?';
200
+ const envelope = await this.request(`${path}${separator}per_page=1000&page=${page}`);
201
+ if (!isItems(envelope.result)) {
202
+ throw new Error(`Cloudflare returned an invalid paginated result for ${path}.`);
203
+ }
204
+ items.push(...envelope.result);
205
+ const totalPages = envelope.result_info?.total_pages ?? page;
206
+ if (page >= totalPages)
207
+ break;
208
+ page += 1;
209
+ }
210
+ return items;
211
+ }
212
+ async result(path, init) {
213
+ return (await this.request(path, init)).result;
214
+ }
215
+ async request(path, init = {}) {
216
+ const response = await this.fetcher(`https://api.cloudflare.com/client/v4${path}`, {
217
+ ...init,
218
+ headers: {
219
+ Authorization: `Bearer ${this.apiToken}`,
220
+ 'Content-Type': 'application/json',
221
+ ...init.headers,
222
+ },
223
+ });
224
+ const body = await response.json();
225
+ if (!isCloudflareEnvelope(body)) {
226
+ throw new CloudflareApiError(`Cloudflare returned a malformed response for ${path}.`, response.status, []);
227
+ }
228
+ if (!response.ok || !body.success) {
229
+ const errors = body.errors ?? [];
230
+ const message = errors
231
+ .map((error) => error.message)
232
+ .filter(Boolean)
233
+ .join('; ') || `HTTP ${response.status}`;
234
+ throw new CloudflareApiError(`Cloudflare API ${path} failed: ${message}`, response.status, errors.flatMap((error) => error.code ?? []));
235
+ }
236
+ return body;
237
+ }
238
+ }
@@ -0,0 +1,48 @@
1
+ import { type CloudflareClient } from './cloudflare.js';
2
+ import { type EnvironmentToken } from './environment.js';
3
+ export interface ProcessResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+ export interface ProcessRunner {
9
+ run(command: string, args: string[], options: {
10
+ cwd: string;
11
+ env?: Record<string, string>;
12
+ }): Promise<ProcessResult>;
13
+ }
14
+ export declare class BunProcessRunner implements ProcessRunner {
15
+ run(command: string, args: string[], options: {
16
+ cwd: string;
17
+ env?: Record<string, string>;
18
+ }): Promise<ProcessResult>;
19
+ }
20
+ export interface WranglerCommandDependencies {
21
+ runner?: ProcessRunner;
22
+ cloudflare?: CloudflareClient;
23
+ processEnv?: NodeJS.ProcessEnv;
24
+ }
25
+ export interface DeployEnvironmentResult {
26
+ environment: EnvironmentToken;
27
+ workerName: string;
28
+ action: 'deployed' | 'activated' | 'remote-cache-hit';
29
+ versionTag?: string;
30
+ }
31
+ export declare function deployEnvironment(cwd: string, environmentValue: string, dependencies?: WranglerCommandDependencies): Promise<DeployEnvironmentResult>;
32
+ export interface CleanupResult {
33
+ environment: `pr${number}`;
34
+ deleted: {
35
+ workers: number;
36
+ routes: number;
37
+ domains: number;
38
+ kvNamespaces: number;
39
+ r2Buckets: number;
40
+ r2Objects: number;
41
+ dnsRecords: number;
42
+ };
43
+ }
44
+ export declare function cleanupPullRequest(cwd: string, prNumber: number, dependencies?: WranglerCommandDependencies): Promise<CleanupResult>;
45
+ export declare function nxTaskVersionTag(environment: NodeJS.ProcessEnv): string | undefined;
46
+ export declare function findVersionIdByTag(value: unknown, tag: string): string | null;
47
+ export declare function isFullCurrentDeployment(value: unknown, versionId: string): boolean;
48
+ //# sourceMappingURL=deploy-environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-environment.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-environment.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,gBAAgB,EAAwB,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAGL,KAAK,gBAAgB,EAQtB,MAAM,kBAAkB,CAAC;AAG1B,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACtH;AAED,qBAAa,gBAAiB,YAAW,aAAa;IAC9C,GAAG,CACP,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GACrD,OAAO,CAAC,aAAa,CAAC,CAcxB;CACF;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,kBAAkB,CAAC;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EACxB,YAAY,GAAE,2BAAgC,GAC7C,OAAO,CAAC,uBAAuB,CAAC,CA2HlC;AACD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,YAAY,GAAE,2BAAgC,GAC7C,OAAO,CAAC,aAAa,CAAC,CA2DxB;AAwFD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,GAAG,SAAS,CAgBnF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7E;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAelF"}
@@ -0,0 +1,383 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import typia from 'typia';
6
+ import { CloudflareRestClient } from './cloudflare.js';
7
+ import { derivePullRequestWranglerConfig, environmentResourceName, hasExactEnvironmentSegment, isPullRequestEnvironment, parseEnvironmentToken, planConfiguredEnvironmentResources, planPullRequestResources, pullRequestEnvironment, } from './environment.js';
8
+ import { parseDevVarsExample } from './prepare-env.js';
9
+ export class BunProcessRunner {
10
+ async run(command, args, options) {
11
+ const child = Bun.spawn([command, ...args], {
12
+ cwd: options.cwd,
13
+ env: { ...process.env, ...options.env },
14
+ stdin: 'inherit',
15
+ stdout: 'pipe',
16
+ stderr: 'pipe',
17
+ });
18
+ const [exitCode, stdout, stderr] = await Promise.all([
19
+ child.exited,
20
+ new Response(child.stdout).text(),
21
+ new Response(child.stderr).text(),
22
+ ]);
23
+ return { exitCode, stdout, stderr };
24
+ }
25
+ }
26
+ const isUnknownRecord = (() => {
27
+ const _io0 = input => Object.keys(input).every(key => {
28
+ const value = input[key];
29
+ if (undefined === value)
30
+ return true;
31
+ return true;
32
+ });
33
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
34
+ })();
35
+ export async function deployEnvironment(cwd, environmentValue, dependencies = {}) {
36
+ const environment = parseEnvironmentToken(environmentValue);
37
+ const processEnv = dependencies.processEnv ?? process.env;
38
+ const accountId = processEnv.CLOUDFLARE_ACCOUNT_ID;
39
+ const apiToken = processEnv.CLOUDFLARE_API_TOKEN;
40
+ if (!accountId)
41
+ throw new Error('CLOUDFLARE_ACCOUNT_ID is required.');
42
+ const cloudflare = dependencies.cloudflare ??
43
+ new CloudflareRestClient(accountId, requiredEnvironmentValue(apiToken, 'CLOUDFLARE_API_TOKEN'));
44
+ const runner = dependencies.runner ?? new BunProcessRunner();
45
+ const committedConfigPath = join(cwd, 'wrangler.toml');
46
+ const committedToml = await readFile(committedConfigPath, 'utf8');
47
+ const secretNames = readSecretNames(cwd);
48
+ const secretValues = {};
49
+ for (const name of secretNames) {
50
+ const value = processEnv[name];
51
+ if (value)
52
+ secretValues[name] = value;
53
+ }
54
+ const missingSecrets = secretNames.filter((name) => !processEnv[name]);
55
+ let configPath = committedConfigPath;
56
+ let temporaryConfigPath;
57
+ let temporarySecretsPath;
58
+ try {
59
+ if (isPullRequestEnvironment(environment)) {
60
+ const staging = planConfiguredEnvironmentResources(committedToml, 'staging');
61
+ if (!staging.workerName.endsWith('-staging')) {
62
+ throw new Error('[env.staging].name must end with the exact suffix -staging.');
63
+ }
64
+ const workerName = environmentResourceName(staging.workerName.slice(0, -'-staging'.length), environment);
65
+ const firstDeployment = !(await cloudflare.listWorkerScripts()).some((script) => script.id === workerName);
66
+ if (firstDeployment && missingSecrets.length > 0) {
67
+ throw new Error(`First deployment of ${workerName} requires process environment values for: ${missingSecrets.join(', ')}.`);
68
+ }
69
+ const liveNamespaces = await cloudflare.listKvNamespaces();
70
+ const plan = planPullRequestResources(committedToml, environment, liveNamespaces);
71
+ const derivedIds = new Map();
72
+ const byTitle = new Map(liveNamespaces.map((namespace) => [namespace.title, namespace]));
73
+ for (const namespace of plan.kvNamespaces) {
74
+ let live = byTitle.get(namespace.title);
75
+ if (!live) {
76
+ try {
77
+ live = await cloudflare.createKvNamespace(namespace.title);
78
+ }
79
+ catch (error) {
80
+ live = (await cloudflare.listKvNamespaces()).find((candidate) => candidate.title === namespace.title);
81
+ if (!live)
82
+ throw error;
83
+ }
84
+ byTitle.set(live.title, live);
85
+ }
86
+ derivedIds.set(namespace.stagingId, live.id);
87
+ }
88
+ const derivedToml = derivePullRequestWranglerConfig(committedToml, {
89
+ environment,
90
+ accountId,
91
+ kvNamespaceIds: derivedIds,
92
+ });
93
+ temporaryConfigPath = join(cwd, `.wrangler.smoo-${process.pid}-${randomUUID()}.toml`);
94
+ await writeFile(temporaryConfigPath, derivedToml, { mode: 0o600 });
95
+ configPath = temporaryConfigPath;
96
+ }
97
+ const toml = configPath === committedConfigPath ? committedToml : await readFile(configPath, 'utf8');
98
+ const plan = planConfiguredEnvironmentResources(toml, environment);
99
+ const workerExists = await reconcileEnvironmentResources(plan, cloudflare);
100
+ const versionTag = nxTaskVersionTag(processEnv);
101
+ const commandContext = { cwd, configPath, environment, workerName: plan.workerName };
102
+ if (versionTag && workerExists) {
103
+ const versions = await wranglerJson(runner, ['versions', 'list', '--name', plan.workerName, '--json'], cwd);
104
+ const deployments = await wranglerJson(runner, ['deployments', 'status', '--name', plan.workerName, '--json'], cwd);
105
+ const versionId = findVersionIdByTag(versions, versionTag);
106
+ if (versionId && isFullCurrentDeployment(deployments, versionId)) {
107
+ return { environment, workerName: plan.workerName, action: 'remote-cache-hit', versionTag };
108
+ }
109
+ if (versionId) {
110
+ await wrangler(runner, [
111
+ 'versions',
112
+ 'deploy',
113
+ '--version-tag',
114
+ versionTag,
115
+ '--name',
116
+ plan.workerName,
117
+ '--config',
118
+ configPath,
119
+ '--env',
120
+ environment,
121
+ '--yes',
122
+ ], cwd);
123
+ return { environment, workerName: plan.workerName, action: 'activated', versionTag };
124
+ }
125
+ }
126
+ const deployArgs = ['deploy', '--config', commandContext.configPath, '--env', commandContext.environment];
127
+ if (versionTag)
128
+ deployArgs.push('--tag', versionTag);
129
+ if (Object.keys(secretValues).length > 0) {
130
+ temporarySecretsPath = join(cwd, `.wrangler-secrets.smoo-${process.pid}-${randomUUID()}.json`);
131
+ await writeFile(temporarySecretsPath, `${JSON.stringify(secretValues)}\n`, { mode: 0o600 });
132
+ deployArgs.push('--secrets-file', temporarySecretsPath);
133
+ }
134
+ await wrangler(runner, deployArgs, cwd);
135
+ return {
136
+ environment,
137
+ workerName: commandContext.workerName,
138
+ action: 'deployed',
139
+ ...(versionTag ? { versionTag } : {}),
140
+ };
141
+ }
142
+ finally {
143
+ if (temporaryConfigPath) {
144
+ await rm(temporaryConfigPath, { force: true });
145
+ }
146
+ if (temporarySecretsPath) {
147
+ await rm(temporarySecretsPath, { force: true });
148
+ }
149
+ }
150
+ }
151
+ export async function cleanupPullRequest(cwd, prNumber, dependencies = {}) {
152
+ const environment = pullRequestEnvironment(prNumber);
153
+ const processEnv = dependencies.processEnv ?? process.env;
154
+ const cloudflare = dependencies.cloudflare ??
155
+ new CloudflareRestClient(requiredEnvironmentValue(processEnv.CLOUDFLARE_ACCOUNT_ID, 'CLOUDFLARE_ACCOUNT_ID'), requiredEnvironmentValue(processEnv.CLOUDFLARE_API_TOKEN, 'CLOUDFLARE_API_TOKEN'));
156
+ void cwd;
157
+ const deleted = {
158
+ workers: 0,
159
+ routes: 0,
160
+ domains: 0,
161
+ kvNamespaces: 0,
162
+ r2Buckets: 0,
163
+ r2Objects: 0,
164
+ dnsRecords: 0,
165
+ };
166
+ for (const domain of await cloudflare.listWorkerDomains()) {
167
+ if (!hasExactEnvironmentSegment(domain.hostname, environment))
168
+ continue;
169
+ await cloudflare.deleteWorkerDomain(domain.id);
170
+ deleted.domains += 1;
171
+ }
172
+ for (const zone of await cloudflare.listZones()) {
173
+ for (const route of await cloudflare.listWorkerRoutes(zone.id)) {
174
+ if (!hasExactEnvironmentSegment(route.pattern, environment))
175
+ continue;
176
+ await cloudflare.deleteWorkerRoute(zone.id, route.id);
177
+ deleted.routes += 1;
178
+ }
179
+ for (const record of await cloudflare.listDnsRecords(zone.id)) {
180
+ if (!hasExactEnvironmentSegment(record.name, environment))
181
+ continue;
182
+ await cloudflare.deleteDnsRecord(zone.id, record.id);
183
+ deleted.dnsRecords += 1;
184
+ }
185
+ }
186
+ for (const script of await cloudflare.listWorkerScripts()) {
187
+ if (!hasExactEnvironmentSegment(script.id, environment))
188
+ continue;
189
+ await cloudflare.deleteWorkerScript(script.id);
190
+ deleted.workers += 1;
191
+ }
192
+ for (const namespace of await cloudflare.listKvNamespaces()) {
193
+ if (!hasExactEnvironmentSegment(namespace.title, environment))
194
+ continue;
195
+ await cloudflare.deleteKvNamespace(namespace.id);
196
+ deleted.kvNamespaces += 1;
197
+ }
198
+ for (const bucket of await cloudflare.listR2Buckets()) {
199
+ if (!hasExactEnvironmentSegment(bucket.name, environment))
200
+ continue;
201
+ for (const key of await cloudflare.listR2Objects(bucket.name)) {
202
+ await cloudflare.deleteR2Object(bucket.name, key);
203
+ deleted.r2Objects += 1;
204
+ }
205
+ await cloudflare.deleteR2Bucket(bucket.name);
206
+ deleted.r2Buckets += 1;
207
+ }
208
+ return { environment, deleted };
209
+ }
210
+ async function reconcileEnvironmentResources(plan, cloudflare) {
211
+ const namespaces = await cloudflare.listKvNamespaces();
212
+ const namespaceIds = new Set(namespaces.map((namespace) => namespace.id));
213
+ for (const binding of plan.kvNamespaces) {
214
+ if (!namespaceIds.has(binding.id)) {
215
+ throw new Error(`KV binding ${binding.binding} references missing namespace ${binding.id}.`);
216
+ }
217
+ }
218
+ const buckets = new Set((await cloudflare.listR2Buckets()).map((bucket) => bucket.name));
219
+ for (const binding of plan.r2Buckets) {
220
+ if (buckets.has(binding.bucketName))
221
+ continue;
222
+ try {
223
+ await cloudflare.createR2Bucket(binding.bucketName);
224
+ }
225
+ catch (error) {
226
+ const exists = (await cloudflare.listR2Buckets()).some((bucket) => bucket.name === binding.bucketName);
227
+ if (!exists)
228
+ throw error;
229
+ }
230
+ buckets.add(binding.bucketName);
231
+ }
232
+ const zones = await cloudflare.listZones();
233
+ const zoneByName = new Map(zones.map((zone) => [zone.name, zone]));
234
+ const dnsNamesByZone = new Map();
235
+ for (const route of plan.routes) {
236
+ if (!route.pattern.startsWith('*.') || !route.zoneName)
237
+ continue;
238
+ const zone = zoneByName.get(route.zoneName);
239
+ if (!zone)
240
+ throw new Error(`Cloudflare zone ${route.zoneName} is not available to the deployment token.`);
241
+ const hostname = route.pattern.slice(0, route.pattern.indexOf('/')).replace(/^\*\./, '');
242
+ const wildcard = `*.${hostname}`;
243
+ let names = dnsNamesByZone.get(zone.id);
244
+ if (!names) {
245
+ names = new Set((await cloudflare.listDnsRecords(zone.id)).map((record) => record.name));
246
+ dnsNamesByZone.set(zone.id, names);
247
+ }
248
+ if (!names.has(wildcard)) {
249
+ try {
250
+ await cloudflare.createDnsRecord(zone.id, wildcard, hostname);
251
+ }
252
+ catch (error) {
253
+ const exists = (await cloudflare.listDnsRecords(zone.id)).some((record) => record.name === wildcard);
254
+ if (!exists)
255
+ throw error;
256
+ }
257
+ names.add(wildcard);
258
+ }
259
+ }
260
+ const scripts = await cloudflare.listWorkerScripts();
261
+ const workerExists = scripts.some((script) => script.id === plan.workerName);
262
+ if (!workerExists)
263
+ return false;
264
+ const domains = await cloudflare.listWorkerDomains();
265
+ for (const route of plan.routes) {
266
+ if (!route.customDomain)
267
+ continue;
268
+ const existing = domains.find((domain) => domain.hostname === route.pattern);
269
+ if (existing?.service === plan.workerName)
270
+ continue;
271
+ if (existing) {
272
+ throw new Error(`Custom domain ${route.pattern} is already attached to ${existing.service ?? 'another Worker'}.`);
273
+ }
274
+ const zone = route.zoneName
275
+ ? zoneByName.get(route.zoneName)
276
+ : zones
277
+ .filter((candidate) => route.pattern === candidate.name || route.pattern.endsWith(`.${candidate.name}`))
278
+ .sort((left, right) => right.name.length - left.name.length)[0];
279
+ if (!zone)
280
+ throw new Error(`No accessible Cloudflare zone contains custom domain ${route.pattern}.`);
281
+ await cloudflare.createWorkerDomain(route.pattern, plan.workerName, zone.id);
282
+ }
283
+ for (const zone of zones) {
284
+ const desired = plan.routes.filter((route) => !route.customDomain && route.zoneName === zone.name);
285
+ if (desired.length === 0)
286
+ continue;
287
+ const routes = await cloudflare.listWorkerRoutes(zone.id);
288
+ for (const route of desired) {
289
+ const existing = routes.find((candidate) => candidate.pattern === route.pattern);
290
+ if (existing?.script === plan.workerName)
291
+ continue;
292
+ if (existing) {
293
+ throw new Error(`Worker route ${route.pattern} is already attached to ${existing.script ?? 'another Worker'}.`);
294
+ }
295
+ await cloudflare.createWorkerRoute(zone.id, route.pattern, plan.workerName);
296
+ }
297
+ }
298
+ return true;
299
+ }
300
+ export function nxTaskVersionTag(environment) {
301
+ const hash = environment.NX_TASK_HASH;
302
+ const underNx = hash !== undefined ||
303
+ environment.NX_TASK_TARGET_PROJECT !== undefined ||
304
+ environment.NX_TASK_TARGET_TARGET !== undefined;
305
+ if (!underNx)
306
+ return undefined;
307
+ if (!hash)
308
+ throw new Error('NX_TASK_HASH is required when deploy-environment runs under Nx.');
309
+ if (/^(?:0|[1-9][0-9]*)$/.test(hash)) {
310
+ return `nx-${hash}`;
311
+ }
312
+ const normalized = hash.toLowerCase();
313
+ if (/^[0-9a-f]{32,}$/.test(normalized)) {
314
+ return `nx-${normalized.slice(0, 32)}`;
315
+ }
316
+ throw new Error('NX_TASK_HASH must be canonical decimal digits or at least 32 hexadecimal characters.');
317
+ }
318
+ export function findVersionIdByTag(value, tag) {
319
+ if (Array.isArray(value)) {
320
+ for (const entry of value) {
321
+ const found = findVersionIdByTag(entry, tag);
322
+ if (found)
323
+ return found;
324
+ }
325
+ return null;
326
+ }
327
+ if (!isUnknownRecord(value))
328
+ return null;
329
+ const annotations = isUnknownRecord(value.annotations) ? value.annotations : undefined;
330
+ const metadata = isUnknownRecord(value.metadata) ? value.metadata : undefined;
331
+ const annotationTag = annotations?.['workers/tag'];
332
+ const candidateTag = typeof annotationTag === 'string' ? annotationTag : typeof value.tag === 'string' ? value.tag : metadata?.tag;
333
+ if (candidateTag === tag) {
334
+ if (typeof value.id === 'string')
335
+ return value.id;
336
+ if (typeof value.version_id === 'string')
337
+ return value.version_id;
338
+ }
339
+ for (const nested of Object.values(value)) {
340
+ const found = findVersionIdByTag(nested, tag);
341
+ if (found)
342
+ return found;
343
+ }
344
+ return null;
345
+ }
346
+ export function isFullCurrentDeployment(value, versionId) {
347
+ if (Array.isArray(value))
348
+ return value.some((entry) => isFullCurrentDeployment(entry, versionId));
349
+ if (!isUnknownRecord(value))
350
+ return false;
351
+ if (Array.isArray(value.versions)) {
352
+ return (value.versions.length === 1 &&
353
+ value.versions.some((version) => isUnknownRecord(version) &&
354
+ (version.version_id === versionId || version.id === versionId) &&
355
+ Number(version.percentage) === 100));
356
+ }
357
+ return Object.values(value).some((entry) => isFullCurrentDeployment(entry, versionId));
358
+ }
359
+ async function wranglerJson(runner, args, cwd) {
360
+ const result = await wrangler(runner, args, cwd);
361
+ try {
362
+ return JSON.parse(result.stdout);
363
+ }
364
+ catch {
365
+ throw new Error(`wrangler ${args.join(' ')} returned invalid JSON.`);
366
+ }
367
+ }
368
+ async function wrangler(runner, args, cwd) {
369
+ const result = await runner.run('wrangler', args, { cwd });
370
+ if (result.exitCode !== 0) {
371
+ throw new Error(`wrangler ${args.join(' ')} failed with exit code ${result.exitCode}: ${result.stderr.trim()}`);
372
+ }
373
+ return result;
374
+ }
375
+ function requiredEnvironmentValue(value, name) {
376
+ if (!value)
377
+ throw new Error(`${name} is required.`);
378
+ return value;
379
+ }
380
+ function readSecretNames(cwd) {
381
+ const path = join(cwd, '.dev.vars.example');
382
+ return existsSync(path) ? parseDevVarsExample(readFileSync(path, 'utf8')) : [];
383
+ }