alchemy 0.70.0 → 0.70.2
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/bin/alchemy.js +97 -60
- package/bin/alchemy.ts +2 -0
- package/bin/commands/telemetry.ts +33 -0
- package/bin/services/execute-alchemy.ts +7 -0
- package/lib/alchemy.d.ts +4 -0
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +1 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/clickhouse/organization.d.ts +1 -1
- package/lib/clickhouse/organization.js +2 -2
- package/lib/clickhouse/organization.js.map +1 -1
- package/lib/clickhouse/service.d.ts +28 -18
- package/lib/clickhouse/service.d.ts.map +1 -1
- package/lib/clickhouse/service.js +61 -9
- package/lib/clickhouse/service.js.map +1 -1
- package/lib/cloudflare/account-api-token.js +1 -1
- package/lib/cloudflare/account-api-token.js.map +1 -1
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +2 -0
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/bun-spa/bun-spa.d.ts +6 -8
- package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -1
- package/lib/cloudflare/bun-spa/bun-spa.js +3 -3
- package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -1
- package/lib/cloudflare/d1-database.d.ts +1 -1
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +1 -1
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +30 -3
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/permission-groups.d.ts +1489 -26
- package/lib/cloudflare/permission-groups.d.ts.map +1 -1
- package/lib/cloudflare/permission-groups.js +1787 -6
- package/lib/cloudflare/permission-groups.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +14 -2
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +4 -1
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker-stub.d.ts +3 -1
- package/lib/cloudflare/worker-stub.d.ts.map +1 -1
- package/lib/cloudflare/worker-stub.js +19 -7
- package/lib/cloudflare/worker-stub.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +74 -10
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +16 -7
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/scope.d.ts +5 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +3 -1
- package/lib/scope.js.map +1 -1
- package/lib/util/camel-to-snake.d.ts +12 -0
- package/lib/util/camel-to-snake.d.ts.map +1 -0
- package/lib/util/camel-to-snake.js +16 -0
- package/lib/util/camel-to-snake.js.map +1 -0
- package/lib/util/idempotent-spawn.js +1 -1
- package/lib/util/idempotent-spawn.js.map +1 -1
- package/lib/util/telemetry.d.ts +3 -2
- package/lib/util/telemetry.d.ts.map +1 -1
- package/lib/util/telemetry.js +34 -10
- package/lib/util/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/src/alchemy.ts +5 -0
- package/src/clickhouse/organization.ts +2 -2
- package/src/clickhouse/service.ts +124 -33
- package/src/cloudflare/account-api-token.ts +1 -1
- package/src/cloudflare/api.ts +2 -0
- package/src/cloudflare/bun-spa/bun-spa.ts +5 -9
- package/src/cloudflare/d1-database.ts +2 -2
- package/src/cloudflare/miniflare/build-worker-options.ts +32 -4
- package/src/cloudflare/permission-groups.ts +1832 -45
- package/src/cloudflare/worker-metadata.ts +20 -3
- package/src/cloudflare/worker-stub.ts +28 -11
- package/src/cloudflare/worker.ts +98 -17
- package/src/scope.ts +7 -0
- package/src/util/camel-to-snake.ts +88 -0
- package/src/util/idempotent-spawn.ts +1 -1
- package/src/util/telemetry.ts +41 -10
- package/templates/tanstack-start/package.json +2 -2
- package/workers/tunnel-proxy.js +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertNever } from "../util/assert-never.ts";
|
|
2
|
+
import { camelToSnakeObjectDeep } from "../util/camel-to-snake.ts";
|
|
2
3
|
import { logger } from "../util/logger.ts";
|
|
3
4
|
import { memoize } from "../util/memoize.ts";
|
|
4
5
|
import { extractCloudflareResult } from "./api-response.ts";
|
|
@@ -180,8 +181,20 @@ export interface WorkerMetadata {
|
|
|
180
181
|
compatibility_date: string;
|
|
181
182
|
compatibility_flags?: string[];
|
|
182
183
|
bindings: WorkerBindingSpec[];
|
|
183
|
-
observability
|
|
184
|
-
enabled
|
|
184
|
+
observability?: {
|
|
185
|
+
enabled?: boolean;
|
|
186
|
+
head_sampling_rate?: number;
|
|
187
|
+
logs?: {
|
|
188
|
+
enabled?: boolean;
|
|
189
|
+
head_sampling_rate?: number;
|
|
190
|
+
invocation_logs?: boolean;
|
|
191
|
+
};
|
|
192
|
+
traces?: {
|
|
193
|
+
enabled?: boolean;
|
|
194
|
+
head_sampling_rate?: number;
|
|
195
|
+
persist?: boolean;
|
|
196
|
+
destinations?: string[];
|
|
197
|
+
};
|
|
185
198
|
};
|
|
186
199
|
logpush?: boolean;
|
|
187
200
|
migrations?: SingleStepMigration;
|
|
@@ -235,6 +248,7 @@ export async function prepareWorkerMetadata(
|
|
|
235
248
|
...(oldSettings?.tags ?? []),
|
|
236
249
|
]),
|
|
237
250
|
);
|
|
251
|
+
|
|
238
252
|
const oldBindings = oldSettings?.bindings;
|
|
239
253
|
// we use Cloudflare Worker tags to store a mapping between Alchemy's stable identifier and the binding name
|
|
240
254
|
// e.g.
|
|
@@ -315,6 +329,8 @@ export async function prepareWorkerMetadata(
|
|
|
315
329
|
return [];
|
|
316
330
|
});
|
|
317
331
|
|
|
332
|
+
const observability = camelToSnakeObjectDeep(props.observability);
|
|
333
|
+
|
|
318
334
|
// Prepare metadata with bindings
|
|
319
335
|
const meta: WorkerMetadata = {
|
|
320
336
|
compatibility_date: props.compatibilityDate,
|
|
@@ -324,7 +340,8 @@ export async function prepareWorkerMetadata(
|
|
|
324
340
|
),
|
|
325
341
|
bindings: [],
|
|
326
342
|
observability: {
|
|
327
|
-
|
|
343
|
+
...observability,
|
|
344
|
+
enabled: observability?.enabled !== false,
|
|
328
345
|
},
|
|
329
346
|
logpush: props.logpush ?? false,
|
|
330
347
|
// TODO(sam): base64 encode instead? 0 collision risk vs readability.
|
|
@@ -128,7 +128,7 @@ export const WorkerStub = Resource("cloudflare::WorkerStub", async function <
|
|
|
128
128
|
} as WorkerStub<RPC>;
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
async function exists(
|
|
131
|
+
export async function exists(
|
|
132
132
|
api: CloudflareApi,
|
|
133
133
|
workerName: string,
|
|
134
134
|
): Promise<boolean> {
|
|
@@ -145,9 +145,10 @@ async function exists(
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
async function createEmptyWorker(
|
|
148
|
+
export async function createEmptyWorker(
|
|
149
149
|
api: CloudflareApi,
|
|
150
150
|
workerName: string,
|
|
151
|
+
version?: string,
|
|
151
152
|
): Promise<void> {
|
|
152
153
|
// Minimal empty worker script
|
|
153
154
|
const emptyScript = `export default {
|
|
@@ -178,6 +179,12 @@ async function createEmptyWorker(
|
|
|
178
179
|
main_module: "worker.js",
|
|
179
180
|
compatibility_date: "2025-04-20",
|
|
180
181
|
bindings: [],
|
|
182
|
+
...(version != null && {
|
|
183
|
+
annotations: {
|
|
184
|
+
"workers/tag": version,
|
|
185
|
+
"workers/message": `Version ${version}`,
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
181
188
|
}),
|
|
182
189
|
],
|
|
183
190
|
{
|
|
@@ -189,14 +196,24 @@ async function createEmptyWorker(
|
|
|
189
196
|
// Upload worker script
|
|
190
197
|
await extractCloudflareResult(
|
|
191
198
|
`create empty worker "${workerName}"`,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
version != null
|
|
200
|
+
? api.post(
|
|
201
|
+
`/accounts/${api.accountId}/workers/scripts/${workerName}/versions`,
|
|
202
|
+
formData,
|
|
203
|
+
{
|
|
204
|
+
headers: {
|
|
205
|
+
"Content-Type": "multipart/form-data",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
: api.put(
|
|
210
|
+
`/accounts/${api.accountId}/workers/scripts/${workerName}`,
|
|
211
|
+
formData,
|
|
212
|
+
{
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": "multipart/form-data",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
),
|
|
201
218
|
);
|
|
202
219
|
}
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
|
|
|
61
61
|
import "../esbuild/bundle.ts";
|
|
62
62
|
import { Scope } from "../scope.ts";
|
|
63
63
|
import type { WorkerRef } from "./worker-ref.ts";
|
|
64
|
+
import { createEmptyWorker, exists } from "./worker-stub.ts";
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Configuration options for static assets
|
|
@@ -166,18 +167,12 @@ export interface BaseWorkerProps<
|
|
|
166
167
|
url?: boolean;
|
|
167
168
|
|
|
168
169
|
/**
|
|
169
|
-
*
|
|
170
|
+
* Specify the observability behavior of the Worker.
|
|
170
171
|
*
|
|
171
|
-
*
|
|
172
|
-
* @default
|
|
172
|
+
* @see https://developers.cloudflare.com/workers/wrangler/configuration/#observability
|
|
173
|
+
* @default - `enabled: true`
|
|
173
174
|
*/
|
|
174
|
-
observability?:
|
|
175
|
-
/**
|
|
176
|
-
* Whether to enable worker logs
|
|
177
|
-
* @default true
|
|
178
|
-
*/
|
|
179
|
-
enabled?: boolean;
|
|
180
|
-
};
|
|
175
|
+
observability?: WorkerObservability;
|
|
181
176
|
|
|
182
177
|
/**
|
|
183
178
|
* Enable Workers Logpush to export trace events (request/response metadata,
|
|
@@ -393,6 +388,85 @@ export interface BaseWorkerProps<
|
|
|
393
388
|
tailConsumers?: Array<Worker | { service: string }>;
|
|
394
389
|
}
|
|
395
390
|
|
|
391
|
+
export interface WorkerObservability {
|
|
392
|
+
/**
|
|
393
|
+
* If observability is enabled for this Worker
|
|
394
|
+
*
|
|
395
|
+
* @default true
|
|
396
|
+
*/
|
|
397
|
+
enabled?: boolean;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* A number between 0 and 1, where 0 indicates zero out of one hundred requests are logged, and 1 indicates every request is logged.
|
|
401
|
+
* If head_sampling_rate is unspecified, it is configured to a default value of 1 (100%).
|
|
402
|
+
* @see https://developers.cloudflare.com/workers/observability/logs/workers-logs/#head-based-sampling
|
|
403
|
+
* @default 1
|
|
404
|
+
*/
|
|
405
|
+
headSamplingRate?: number;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Configuration for worker logs
|
|
409
|
+
*/
|
|
410
|
+
logs?: {
|
|
411
|
+
/**
|
|
412
|
+
* Whether logs are enabled
|
|
413
|
+
* @default true
|
|
414
|
+
*/
|
|
415
|
+
enabled?: boolean;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* The sampling rate for logs
|
|
419
|
+
*/
|
|
420
|
+
headSamplingRate?: number;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Set to false to disable invocation logs
|
|
424
|
+
* @default true
|
|
425
|
+
*/
|
|
426
|
+
invocationLogs?: boolean;
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* If logs should be persisted to the Cloudflare observability platform where they can be queried in the dashboard.
|
|
430
|
+
* @default true
|
|
431
|
+
*/
|
|
432
|
+
persist?: boolean;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* What destinations logs emitted from the Worker should be sent to.
|
|
436
|
+
* @default []
|
|
437
|
+
*/
|
|
438
|
+
destinations?: string[];
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Configuration for worker traces
|
|
443
|
+
*/
|
|
444
|
+
traces?: {
|
|
445
|
+
/**
|
|
446
|
+
* Whether traces are enabled
|
|
447
|
+
* @default true
|
|
448
|
+
*/
|
|
449
|
+
enabled?: boolean;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The sampling rate for traces
|
|
453
|
+
*/
|
|
454
|
+
headSamplingRate?: number;
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* If traces should be persisted to the Cloudflare observability platform where they can be queried in the dashboard.
|
|
458
|
+
* @default true
|
|
459
|
+
*/
|
|
460
|
+
persist?: boolean;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* What destinations traces emitted from the Worker should be sent to.
|
|
464
|
+
* @default []
|
|
465
|
+
*/
|
|
466
|
+
destinations?: string[];
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
396
470
|
export interface InlineWorkerProps<
|
|
397
471
|
B extends Bindings | undefined = Bindings,
|
|
398
472
|
RPC extends Rpc.WorkerEntrypointBranded = Rpc.WorkerEntrypointBranded,
|
|
@@ -863,13 +937,20 @@ const _Worker = Resource(
|
|
|
863
937
|
durableObjects: options.durableObjects,
|
|
864
938
|
workflows: options.workflows,
|
|
865
939
|
});
|
|
866
|
-
if (
|
|
940
|
+
if (this.output?.dev?.hasRemote !== false) {
|
|
867
941
|
const api = await createCloudflareApi(props);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
942
|
+
if (props.version) {
|
|
943
|
+
//* if the worker exists we deploy an empty version so we can destroy
|
|
944
|
+
if (await exists(api, options.name)) {
|
|
945
|
+
await createEmptyWorker(api, options.name, props.version);
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
await deleteQueueConsumers(api, options.name);
|
|
949
|
+
await deleteWorker(api, {
|
|
950
|
+
scriptName: options.name,
|
|
951
|
+
dispatchNamespace: options.dispatchNamespace,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
873
954
|
}
|
|
874
955
|
return this.destroy();
|
|
875
956
|
}
|
|
@@ -1607,7 +1688,7 @@ async function assertWorkerDoesNotExist(
|
|
|
1607
1688
|
);
|
|
1608
1689
|
}
|
|
1609
1690
|
|
|
1610
|
-
async function getScriptMetadata(
|
|
1691
|
+
export async function getScriptMetadata(
|
|
1611
1692
|
api: CloudflareApi,
|
|
1612
1693
|
scriptName: string,
|
|
1613
1694
|
): Promise<WorkerScriptMetadata | undefined> {
|
package/src/scope.ts
CHANGED
|
@@ -103,6 +103,10 @@ export interface ScopeOptions extends ProviderCredentials {
|
|
|
103
103
|
* @default process.cwd()
|
|
104
104
|
*/
|
|
105
105
|
rootDir?: string;
|
|
106
|
+
/**
|
|
107
|
+
* The Alchemy profile to use for authoriziing requests.
|
|
108
|
+
*/
|
|
109
|
+
profile?: string;
|
|
106
110
|
/**
|
|
107
111
|
* Whether this is the application that was selected with `--app`
|
|
108
112
|
*
|
|
@@ -203,6 +207,7 @@ export class Scope {
|
|
|
203
207
|
public readonly rootDir: string;
|
|
204
208
|
public readonly dotAlchemy: string;
|
|
205
209
|
public readonly isSelected: boolean | undefined;
|
|
210
|
+
public readonly profile: string | undefined;
|
|
206
211
|
|
|
207
212
|
// Provider credentials for scope-level credential overrides
|
|
208
213
|
public readonly providerCredentials: ProviderCredentials;
|
|
@@ -242,6 +247,7 @@ export class Scope {
|
|
|
242
247
|
rootDir,
|
|
243
248
|
isSelected,
|
|
244
249
|
noTrack,
|
|
250
|
+
profile,
|
|
245
251
|
...providerCredentials
|
|
246
252
|
} = options;
|
|
247
253
|
|
|
@@ -268,6 +274,7 @@ export class Scope {
|
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
this.stage = stage ?? this.parent?.stage ?? DEFAULT_STAGE;
|
|
277
|
+
this.profile = profile ?? this.parent?.profile;
|
|
271
278
|
this.parent?.children.set(this.scopeName!, this);
|
|
272
279
|
this.quiet = quiet ?? this.parent?.quiet ?? false;
|
|
273
280
|
if (this.parent && !this.scopeName) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export interface LogPushProps {
|
|
2
|
+
maxFooBar: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function camelToSnakeObjectDeep<T extends object | undefined>(
|
|
6
|
+
obj: T,
|
|
7
|
+
): T extends undefined ? undefined : CamelToSnake<T> {
|
|
8
|
+
return (
|
|
9
|
+
obj
|
|
10
|
+
? Object.fromEntries(
|
|
11
|
+
Object.entries(obj).map(([key, value]) => [
|
|
12
|
+
key
|
|
13
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") // Handle consecutive capitals: "FOOBar" -> "FOO_Bar"
|
|
14
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2") // Handle normal camelCase: "fooBar" -> "foo_Bar"
|
|
15
|
+
.toLowerCase(),
|
|
16
|
+
Array.isArray(value)
|
|
17
|
+
? value.map(camelToSnakeObjectDeep)
|
|
18
|
+
: typeof value === "object" && value !== null
|
|
19
|
+
? camelToSnakeObjectDeep(value)
|
|
20
|
+
: value,
|
|
21
|
+
]),
|
|
22
|
+
)
|
|
23
|
+
: undefined
|
|
24
|
+
) as T extends undefined ? undefined : CamelToSnake<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper to check if a character is uppercase
|
|
28
|
+
type IsUpper<C extends string> = C extends Uppercase<C>
|
|
29
|
+
? C extends Lowercase<C>
|
|
30
|
+
? false // Not a letter
|
|
31
|
+
: true // Uppercase letter
|
|
32
|
+
: false;
|
|
33
|
+
|
|
34
|
+
// Helper to check if a character is lowercase
|
|
35
|
+
type IsLower<C extends string> = C extends Lowercase<C>
|
|
36
|
+
? C extends Uppercase<C>
|
|
37
|
+
? false // Not a letter
|
|
38
|
+
: true // Lowercase letter
|
|
39
|
+
: false;
|
|
40
|
+
|
|
41
|
+
// Convert a camelCase string to snake_case at the type level
|
|
42
|
+
type CamelToSnakeString<
|
|
43
|
+
S extends string,
|
|
44
|
+
Acc extends string = "",
|
|
45
|
+
> = S extends `${infer First}${infer Second}${infer Rest}`
|
|
46
|
+
? IsUpper<First> extends true
|
|
47
|
+
? IsUpper<Second> extends true
|
|
48
|
+
? // FOO... pattern - check if next char is lowercase
|
|
49
|
+
Rest extends `${infer Third}${infer _}`
|
|
50
|
+
? IsLower<Third> extends true
|
|
51
|
+
? // FOOBar -> add First, add _, add Second lowercase, continue with Rest
|
|
52
|
+
CamelToSnakeString<
|
|
53
|
+
Rest,
|
|
54
|
+
`${Acc}${Lowercase<First>}_${Lowercase<Second>}`
|
|
55
|
+
>
|
|
56
|
+
: // FOO -> continue
|
|
57
|
+
CamelToSnakeString<`${Second}${Rest}`, `${Acc}${Lowercase<First>}`>
|
|
58
|
+
: // Only two chars left, both upper
|
|
59
|
+
CamelToSnakeString<`${Second}${Rest}`, `${Acc}${Lowercase<First>}`>
|
|
60
|
+
: // FooBar or Foo - add underscore before uppercase if not at start
|
|
61
|
+
CamelToSnakeString<
|
|
62
|
+
`${Second}${Rest}`,
|
|
63
|
+
Acc extends "" ? `${Lowercase<First>}` : `${Acc}_${Lowercase<First>}`
|
|
64
|
+
>
|
|
65
|
+
: IsLower<First> extends true
|
|
66
|
+
? IsUpper<Second> extends true
|
|
67
|
+
? // fooBar -> just add the lowercase char, underscore will be added when processing uppercase
|
|
68
|
+
CamelToSnakeString<`${Second}${Rest}`, `${Acc}${First}`>
|
|
69
|
+
: // foobar -> continue
|
|
70
|
+
CamelToSnakeString<`${Second}${Rest}`, `${Acc}${First}`>
|
|
71
|
+
: // Number or special char
|
|
72
|
+
CamelToSnakeString<`${Second}${Rest}`, `${Acc}${First}`>
|
|
73
|
+
: S extends `${infer Last}`
|
|
74
|
+
? `${Acc}${Lowercase<Last>}`
|
|
75
|
+
: Acc;
|
|
76
|
+
|
|
77
|
+
// Deep transformation for objects
|
|
78
|
+
type CamelToSnake<T> = T extends object
|
|
79
|
+
? T extends Array<infer U>
|
|
80
|
+
? Array<CamelToSnake<U>>
|
|
81
|
+
: T extends Date | RegExp | Function
|
|
82
|
+
? T // Don't transform special object types
|
|
83
|
+
: {
|
|
84
|
+
[K in keyof T as K extends string
|
|
85
|
+
? CamelToSnakeString<K>
|
|
86
|
+
: K]: CamelToSnake<T[K]>;
|
|
87
|
+
}
|
|
88
|
+
: T;
|
|
@@ -112,7 +112,7 @@ export async function idempotentSpawn({
|
|
|
112
112
|
const child = spawn(cmd, {
|
|
113
113
|
shell: true,
|
|
114
114
|
cwd,
|
|
115
|
-
stdio: ["
|
|
115
|
+
stdio: ["inherit", out.fd, out.fd], // stdout/stderr -> files (OS-level)
|
|
116
116
|
env,
|
|
117
117
|
detached: false,
|
|
118
118
|
});
|
package/src/util/telemetry.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import envPaths from "env-paths";
|
|
2
2
|
import { exec } from "node:child_process";
|
|
3
|
-
import
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "pathe";
|
|
6
6
|
import pkg from "../../package.json" with { type: "json" };
|
|
@@ -9,11 +9,13 @@ import { Scope } from "../scope.ts";
|
|
|
9
9
|
import { logger } from "./logger.ts";
|
|
10
10
|
import { memoize } from "./memoize.ts";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const ALCHEMY_DIR = path.join(os.homedir(), ".alchemy");
|
|
13
|
+
const ID_PATH = path.join(ALCHEMY_DIR, "id");
|
|
14
|
+
const ID_PATH_LEGACY = path.join(
|
|
14
15
|
envPaths("alchemy", { suffix: "" }).config,
|
|
15
16
|
"id",
|
|
16
17
|
);
|
|
18
|
+
const DISABLED_PATH = path.join(ALCHEMY_DIR, "telemetry-disabled");
|
|
17
19
|
|
|
18
20
|
export const TELEMETRY_DISABLED =
|
|
19
21
|
!!process.env.ALCHEMY_TELEMETRY_DISABLED || !!process.env.DO_NOT_TRACK;
|
|
@@ -23,26 +25,43 @@ export const TELEMETRY_API_URL =
|
|
|
23
25
|
export const SUPPRESS_TELEMETRY_ERRORS =
|
|
24
26
|
!!process.env.ALCHEMY_TELEMETRY_SUPPRESS_ERRORS;
|
|
25
27
|
|
|
28
|
+
export const getGlobalTelemetryDisabled = memoize(async () => {
|
|
29
|
+
const disabled = await fs
|
|
30
|
+
.readFile(DISABLED_PATH, "utf-8")
|
|
31
|
+
.then((data) => data.trim() === "true")
|
|
32
|
+
.catch(() => false);
|
|
33
|
+
return disabled;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export async function setGlobalTelemetryDisabled() {
|
|
37
|
+
await fs.mkdir(ALCHEMY_DIR, { recursive: true });
|
|
38
|
+
await fs.writeFile(DISABLED_PATH, "true");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function setGlobalTelemetryEnabled() {
|
|
42
|
+
await fs.rm(DISABLED_PATH, { force: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
async function getOrCreateUserId() {
|
|
27
46
|
async function readUserId(path: string) {
|
|
28
47
|
try {
|
|
29
|
-
return (await readFile(path, "utf-8")).trim();
|
|
48
|
+
return (await fs.readFile(path, "utf-8")).trim();
|
|
30
49
|
} catch {
|
|
31
50
|
return null;
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
const id = await readUserId(
|
|
54
|
+
const id = await readUserId(ID_PATH);
|
|
36
55
|
if (id) {
|
|
37
56
|
return id;
|
|
38
57
|
}
|
|
39
58
|
|
|
40
|
-
const legacyId = await readUserId(
|
|
59
|
+
const legacyId = await readUserId(ID_PATH_LEGACY);
|
|
41
60
|
|
|
42
61
|
try {
|
|
43
62
|
const id = legacyId ?? crypto.randomUUID();
|
|
44
|
-
await mkdir(
|
|
45
|
-
await writeFile(
|
|
63
|
+
await fs.mkdir(ALCHEMY_DIR, { recursive: true });
|
|
64
|
+
await fs.writeFile(ID_PATH, id);
|
|
46
65
|
if (!legacyId) {
|
|
47
66
|
console.warn(
|
|
48
67
|
[
|
|
@@ -59,7 +78,11 @@ async function getOrCreateUserId() {
|
|
|
59
78
|
|
|
60
79
|
async function getRootCommitHash() {
|
|
61
80
|
return new Promise<string | null>((resolve) => {
|
|
62
|
-
|
|
81
|
+
const command =
|
|
82
|
+
process.platform === "win32"
|
|
83
|
+
? `git rev-list --max-parents=0 HEAD | ForEach-Object { if (-not (git cat-file -p $_ | Select-String "^parent ")) { $_ } }`
|
|
84
|
+
: `git rev-list --max-parents=0 HEAD | xargs -r -I{} sh -c 'git cat-file -p {} | grep -q "^parent " || echo {}'`;
|
|
85
|
+
exec(command, (err, stdout) => {
|
|
63
86
|
if (err) {
|
|
64
87
|
resolve(null);
|
|
65
88
|
return;
|
|
@@ -236,6 +259,14 @@ export type AlchemyTelemetryData = {
|
|
|
236
259
|
duration: number;
|
|
237
260
|
};
|
|
238
261
|
|
|
262
|
+
async function isTelemetryDisabled() {
|
|
263
|
+
return (
|
|
264
|
+
Scope.getScope()?.noTrack ||
|
|
265
|
+
TELEMETRY_DISABLED ||
|
|
266
|
+
(await getGlobalTelemetryDisabled())
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
239
270
|
export async function createAndSendEvent(
|
|
240
271
|
data:
|
|
241
272
|
| CliTelemetryData
|
|
@@ -244,7 +275,7 @@ export async function createAndSendEvent(
|
|
|
244
275
|
| AlchemyTelemetryData,
|
|
245
276
|
error?: Error,
|
|
246
277
|
) {
|
|
247
|
-
if (
|
|
278
|
+
if (await isTelemetryDisabled()) {
|
|
248
279
|
return;
|
|
249
280
|
}
|
|
250
281
|
try {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"vite-tsconfig-paths": "^5.1.4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@cloudflare/vite-plugin": "
|
|
28
|
-
"@cloudflare/workers-types": "
|
|
27
|
+
"@cloudflare/vite-plugin": "^1.11.6",
|
|
28
|
+
"@cloudflare/workers-types": "^4.20250805.0",
|
|
29
29
|
"@testing-library/dom": "^10.4.0",
|
|
30
30
|
"@testing-library/react": "^16.2.0",
|
|
31
31
|
"@types/node": "^22.10.2",
|
package/workers/tunnel-proxy.js
CHANGED
|
@@ -34,7 +34,7 @@ var h={async fetch(e,r){let n=new URL(e.url);n.host=r.TUNNEL_HOST;let l=new Head
|
|
|
34
34
|
Alchemy</a>.</p>
|
|
35
35
|
</div>
|
|
36
36
|
<div class="bg-slate-200 px-5 py-3 flex flex-col w-full max-w-lg">
|
|
37
|
-
<p class="text-sm text-slate-500">Alchemy 0.70.
|
|
37
|
+
<p class="text-sm text-slate-500">Alchemy 0.70.2</p>
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
40
40
|
</body>
|