lopata 0.0.1
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/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { CliContext } from "./context";
|
|
2
|
+
import { parseFlag } from "./context";
|
|
3
|
+
import { FileR2Bucket } from "../bindings/r2";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse wrangler-compatible objectPath in the form {bucket}/{key}.
|
|
7
|
+
* If there's no slash, the whole string is the bucket name (for list).
|
|
8
|
+
*/
|
|
9
|
+
function parseObjectPath(objectPath: string): { bucketName: string; key: string } {
|
|
10
|
+
const idx = objectPath.indexOf("/");
|
|
11
|
+
if (idx === -1) return { bucketName: objectPath, key: "" };
|
|
12
|
+
return { bucketName: objectPath.slice(0, idx), key: objectPath.slice(idx + 1) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve bucket name: check it exists in config, return the config entry's bucket_name.
|
|
17
|
+
*/
|
|
18
|
+
function resolveBucket(
|
|
19
|
+
buckets: { binding: string; bucket_name: string }[] | undefined,
|
|
20
|
+
bucketName: string,
|
|
21
|
+
): string {
|
|
22
|
+
if (!buckets || buckets.length === 0) {
|
|
23
|
+
console.error("No R2 buckets configured.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// Match by bucket_name or binding name
|
|
27
|
+
const match = buckets.find(b => b.bucket_name === bucketName || b.binding === bucketName);
|
|
28
|
+
if (!match) {
|
|
29
|
+
const names = buckets.map(b => b.bucket_name).join(", ");
|
|
30
|
+
console.error(`R2 bucket "${bucketName}" not found. Available: ${names}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return match.bucket_name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function run(ctx: CliContext, args: string[]) {
|
|
37
|
+
const sub = args[0];
|
|
38
|
+
if (sub !== "object") {
|
|
39
|
+
console.error(`Usage: bunflare r2 object <list|get|put|delete> <bucket/key>`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const action = args[1];
|
|
44
|
+
const objectPath = args[2];
|
|
45
|
+
const config = await ctx.config();
|
|
46
|
+
|
|
47
|
+
switch (action) {
|
|
48
|
+
case "list": {
|
|
49
|
+
if (!objectPath) {
|
|
50
|
+
// No path — list all buckets if no path given
|
|
51
|
+
const buckets = config.r2_buckets ?? [];
|
|
52
|
+
if (buckets.length === 0) {
|
|
53
|
+
console.log("No R2 buckets configured.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const b of buckets) {
|
|
57
|
+
console.log(`${b.bucket_name} binding=${b.binding}`);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { bucketName, key: prefix } = parseObjectPath(objectPath);
|
|
62
|
+
const resolved = resolveBucket(config.r2_buckets, bucketName);
|
|
63
|
+
const bucket = new FileR2Bucket(ctx.db(), resolved, ctx.dataDir());
|
|
64
|
+
const listPrefix = parseFlag(ctx.args, "--prefix") ?? prefix;
|
|
65
|
+
let cursor = "";
|
|
66
|
+
let total = 0;
|
|
67
|
+
do {
|
|
68
|
+
const result = await bucket.list({ prefix: listPrefix, cursor: cursor || undefined });
|
|
69
|
+
for (const obj of result.objects) {
|
|
70
|
+
const size = formatSize(obj.size);
|
|
71
|
+
const date = obj.uploaded.toISOString().slice(0, 19).replace("T", " ");
|
|
72
|
+
console.log(`${date} ${size.padStart(10)} ${obj.key}`);
|
|
73
|
+
}
|
|
74
|
+
total += result.objects.length;
|
|
75
|
+
cursor = result.cursor;
|
|
76
|
+
} while (cursor);
|
|
77
|
+
if (total === 0) console.log("(no objects)");
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "get": {
|
|
81
|
+
if (!objectPath || !objectPath.includes("/")) {
|
|
82
|
+
console.error("Usage: bunflare r2 object get <bucket/key>");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const { bucketName, key } = parseObjectPath(objectPath);
|
|
86
|
+
const resolved = resolveBucket(config.r2_buckets, bucketName);
|
|
87
|
+
const bucket = new FileR2Bucket(ctx.db(), resolved, ctx.dataDir());
|
|
88
|
+
const obj = await bucket.get(key);
|
|
89
|
+
if (!obj) {
|
|
90
|
+
console.error(`Object not found: ${key}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
if ("arrayBuffer" in obj) {
|
|
94
|
+
const data = await obj.arrayBuffer();
|
|
95
|
+
process.stdout.write(new Uint8Array(data));
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "put": {
|
|
100
|
+
if (!objectPath || !objectPath.includes("/")) {
|
|
101
|
+
console.error("Usage: bunflare r2 object put <bucket/key> --file <path>");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const filePath = parseFlag(ctx.args, "--file") ?? parseFlag(ctx.args, "-f");
|
|
105
|
+
if (!filePath) {
|
|
106
|
+
console.error("Usage: bunflare r2 object put <bucket/key> --file <path>");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const { bucketName, key } = parseObjectPath(objectPath);
|
|
110
|
+
const resolved = resolveBucket(config.r2_buckets, bucketName);
|
|
111
|
+
const bucket = new FileR2Bucket(ctx.db(), resolved, ctx.dataDir());
|
|
112
|
+
const data = await Bun.file(filePath).arrayBuffer();
|
|
113
|
+
await bucket.put(key, data);
|
|
114
|
+
console.log(`Uploaded ${key} (${formatSize(data.byteLength)})`);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "delete": {
|
|
118
|
+
if (!objectPath || !objectPath.includes("/")) {
|
|
119
|
+
console.error("Usage: bunflare r2 object delete <bucket/key>");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const { bucketName, key } = parseObjectPath(objectPath);
|
|
123
|
+
const resolved = resolveBucket(config.r2_buckets, bucketName);
|
|
124
|
+
const bucket = new FileR2Bucket(ctx.db(), resolved, ctx.dataDir());
|
|
125
|
+
await bucket.delete(key);
|
|
126
|
+
console.log(`Deleted ${key}`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
console.error(`Usage: bunflare r2 object <list|get|put|delete> <bucket/key>`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatSize(bytes: number): string {
|
|
136
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
137
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
138
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
139
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
140
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { CliContext } from "./context";
|
|
2
|
+
import { parseFlag, hasFlag } from "./context";
|
|
3
|
+
import { getTracingDatabase } from "../tracing/db";
|
|
4
|
+
import { TraceStore } from "../tracing/store";
|
|
5
|
+
import type { SpanData, SpanEventData } from "../tracing/types";
|
|
6
|
+
|
|
7
|
+
const USAGE = `Usage:
|
|
8
|
+
bunflare trace list [options] List traces
|
|
9
|
+
bunflare trace get <traceId> Get trace detail
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--json Output as JSON
|
|
13
|
+
--limit <n> Max results (default 50)
|
|
14
|
+
--since <dur> Show traces since duration ago (e.g. 5m, 1h, 2d)
|
|
15
|
+
--search <query> Filter traces by text search
|
|
16
|
+
--cursor <cursor> Pagination cursor from previous result`;
|
|
17
|
+
|
|
18
|
+
export async function run(ctx: CliContext, args: string[]) {
|
|
19
|
+
const action = args[0];
|
|
20
|
+
const db = getTracingDatabase();
|
|
21
|
+
const store = new TraceStore(db);
|
|
22
|
+
const json = hasFlag(ctx.args, "--json");
|
|
23
|
+
|
|
24
|
+
switch (action) {
|
|
25
|
+
case "list": {
|
|
26
|
+
const limitStr = parseFlag(ctx.args, "--limit");
|
|
27
|
+
const limit = limitStr ? parseInt(limitStr, 10) : 50;
|
|
28
|
+
const since = parseFlag(ctx.args, "--since");
|
|
29
|
+
const search = parseFlag(ctx.args, "--search");
|
|
30
|
+
const cursor = parseFlag(ctx.args, "--cursor");
|
|
31
|
+
|
|
32
|
+
let items: Array<{ traceId: string; rootSpanName: string; workerName: string | null; status: string; statusMessage: string | null; startTime: number; durationMs: number | null; spanCount: number; errorCount: number }>;
|
|
33
|
+
let nextCursor: string | null;
|
|
34
|
+
|
|
35
|
+
if (search) {
|
|
36
|
+
const result = store.searchTraces(search, limit);
|
|
37
|
+
items = result.items;
|
|
38
|
+
nextCursor = result.cursor;
|
|
39
|
+
} else if (since) {
|
|
40
|
+
const sinceMs = Date.now() - parseDuration(since);
|
|
41
|
+
items = store.getRecentTraces(sinceMs, limit);
|
|
42
|
+
nextCursor = null;
|
|
43
|
+
} else {
|
|
44
|
+
const result = store.listTraces({ limit, cursor });
|
|
45
|
+
items = result.items;
|
|
46
|
+
nextCursor = result.cursor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (json) {
|
|
50
|
+
console.log(JSON.stringify({ items, cursor: nextCursor }, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
printTraceList(items, nextCursor);
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "get": {
|
|
57
|
+
const traceId = args[1];
|
|
58
|
+
if (!traceId) {
|
|
59
|
+
console.error("Usage: bunflare trace get <traceId>");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { spans, events } = store.getTrace(traceId);
|
|
64
|
+
if (spans.length === 0) {
|
|
65
|
+
console.error(`Trace not found: ${traceId}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const errors = store.getErrorsForTrace(traceId);
|
|
70
|
+
|
|
71
|
+
if (json) {
|
|
72
|
+
console.log(JSON.stringify({ traceId, spans, events, errors }, null, 2));
|
|
73
|
+
} else {
|
|
74
|
+
printTraceDetail(traceId, spans, events, errors);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
default:
|
|
79
|
+
console.error(USAGE);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Text output: list ───────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function printTraceList(
|
|
87
|
+
items: Array<{ traceId: string; rootSpanName: string; status: string; startTime: number; durationMs: number | null; spanCount: number; errorCount: number }>,
|
|
88
|
+
cursor: string | null,
|
|
89
|
+
) {
|
|
90
|
+
if (items.length === 0) {
|
|
91
|
+
console.log("(no traces)");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const header = "TIME STATUS DURATION SPANS ERRORS NAME";
|
|
96
|
+
const sep = "─".repeat(header.length);
|
|
97
|
+
console.log(header);
|
|
98
|
+
console.log(sep);
|
|
99
|
+
|
|
100
|
+
for (const t of items) {
|
|
101
|
+
const time = fmtTime(t.startTime);
|
|
102
|
+
const status = statusIcon(t.status) + " " + t.status.padEnd(5);
|
|
103
|
+
const dur = t.durationMs !== null ? `${t.durationMs.toFixed(0)}ms`.padStart(6) : " ...";
|
|
104
|
+
const spans = String(t.spanCount).padStart(5);
|
|
105
|
+
const errs = t.errorCount > 0 ? String(t.errorCount).padStart(6) : " -";
|
|
106
|
+
console.log(`${time} ${status} ${dur} ${spans} ${errs} ${t.rootSpanName}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(sep);
|
|
110
|
+
console.log(`${items.length} trace(s)` + (items.length > 0 ? ` oldest id: ${items[items.length - 1]!.traceId}` : ""));
|
|
111
|
+
|
|
112
|
+
if (cursor) {
|
|
113
|
+
console.log(`\nMore results available. Use --cursor ${cursor}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Text output: get ────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
function printTraceDetail(
|
|
120
|
+
traceId: string,
|
|
121
|
+
spans: SpanData[],
|
|
122
|
+
events: SpanEventData[],
|
|
123
|
+
errors: Array<{ id: string; timestamp: number; errorName: string; errorMessage: string; source: string | null; data: unknown }>,
|
|
124
|
+
) {
|
|
125
|
+
const root = spans.find(s => !s.parentSpanId);
|
|
126
|
+
const totalDuration = root?.durationMs;
|
|
127
|
+
const startTime = root?.startTime ?? spans[0]!.startTime;
|
|
128
|
+
|
|
129
|
+
// Header
|
|
130
|
+
console.log(`Trace ${traceId}`);
|
|
131
|
+
console.log(`Status: ${statusIcon(root?.status ?? "unset")} ${root?.status ?? "unset"} | Duration: ${fmtDuration(totalDuration)} | Spans: ${spans.length} | Started: ${fmtTime(startTime)}`);
|
|
132
|
+
if (root?.workerName) console.log(`Worker: ${root.workerName}`);
|
|
133
|
+
|
|
134
|
+
// Span tree
|
|
135
|
+
console.log("\nSpans");
|
|
136
|
+
const spanMap = new Map(spans.map(s => [s.spanId, s]));
|
|
137
|
+
const children = new Map<string | null, SpanData[]>();
|
|
138
|
+
for (const s of spans) {
|
|
139
|
+
const pid = s.parentSpanId ?? null;
|
|
140
|
+
if (!children.has(pid)) children.set(pid, []);
|
|
141
|
+
children.get(pid)!.push(s);
|
|
142
|
+
}
|
|
143
|
+
const rootSpans = children.get(null) ?? [];
|
|
144
|
+
for (let i = 0; i < rootSpans.length; i++) {
|
|
145
|
+
printSpanTree(rootSpans[i]!, children, "", i === rootSpans.length - 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Events
|
|
149
|
+
if (events.length > 0) {
|
|
150
|
+
console.log("\nEvents");
|
|
151
|
+
for (const e of events) {
|
|
152
|
+
const ts = fmtTimestamp(e.timestamp);
|
|
153
|
+
const level = e.level ? `[${e.level}]`.padEnd(8) : " ";
|
|
154
|
+
const msg = e.message ?? e.name;
|
|
155
|
+
const spanName = spanMap.get(e.spanId)?.name;
|
|
156
|
+
const spanRef = spanName ? ` (${spanName})` : "";
|
|
157
|
+
console.log(` ${ts} ${level} ${msg}${spanRef}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Errors
|
|
162
|
+
if (errors.length > 0) {
|
|
163
|
+
console.log("\nErrors");
|
|
164
|
+
for (const e of errors) {
|
|
165
|
+
const ts = fmtTimestamp(e.timestamp);
|
|
166
|
+
console.log(` ${ts} \x1b[31m${e.errorName}: ${e.errorMessage}\x1b[0m` + (e.source ? ` [${e.source}]` : ""));
|
|
167
|
+
const stack = extractStack(e.data);
|
|
168
|
+
if (stack) {
|
|
169
|
+
for (const line of stack) {
|
|
170
|
+
console.log(` \x1b[2m${line}\x1b[0m`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printSpanTree(span: SpanData, children: Map<string | null, SpanData[]>, prefix: string, isLast: boolean) {
|
|
178
|
+
const connector = isLast ? "└─ " : "├─ ";
|
|
179
|
+
const status = statusIcon(span.status);
|
|
180
|
+
const dur = fmtDuration(span.durationMs);
|
|
181
|
+
const kind = span.kind !== "internal" ? ` [${span.kind}]` : "";
|
|
182
|
+
const errMsg = span.status === "error" && span.statusMessage ? `: ${span.statusMessage}` : "";
|
|
183
|
+
console.log(`${prefix}${connector}${status} ${span.name}${kind} ${span.status} ${dur}${errMsg}`);
|
|
184
|
+
|
|
185
|
+
const attrs = span.attributes;
|
|
186
|
+
if (attrs && Object.keys(attrs).length > 0) {
|
|
187
|
+
const attrPrefix = prefix + (isLast ? " " : "│ ");
|
|
188
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
189
|
+
console.log(`${attrPrefix} ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const kids = children.get(span.spanId) ?? [];
|
|
194
|
+
const childPrefix = prefix + (isLast ? " " : "│ ");
|
|
195
|
+
for (let i = 0; i < kids.length; i++) {
|
|
196
|
+
printSpanTree(kids[i]!, children, childPrefix, i === kids.length - 1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Formatting helpers ──────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function statusIcon(status: string): string {
|
|
203
|
+
switch (status) {
|
|
204
|
+
case "ok": return "\x1b[32m✓\x1b[0m";
|
|
205
|
+
case "error": return "\x1b[31m✗\x1b[0m";
|
|
206
|
+
default: return "\x1b[33m●\x1b[0m";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function fmtTime(ms: number): string {
|
|
211
|
+
return new Date(ms).toISOString().slice(0, 19).replace("T", " ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fmtTimestamp(ms: number): string {
|
|
215
|
+
return new Date(ms).toISOString().slice(11, 23);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fmtDuration(ms: number | null | undefined): string {
|
|
219
|
+
if (ms == null) return "...";
|
|
220
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`;
|
|
221
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
222
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractStack(data: unknown): string[] | null {
|
|
226
|
+
if (!data || typeof data !== "object") return null;
|
|
227
|
+
const d = data as Record<string, unknown>;
|
|
228
|
+
const error = d.error as Record<string, unknown> | undefined;
|
|
229
|
+
const stack = error?.stack;
|
|
230
|
+
if (typeof stack !== "string") return null;
|
|
231
|
+
// Stack lines start with " at " — skip the first line (error name: message, already printed)
|
|
232
|
+
const lines = stack.split("\n");
|
|
233
|
+
const frameLines = lines.filter(l => l.trimStart().startsWith("at "));
|
|
234
|
+
return frameLines.length > 0 ? frameLines.map(l => l.trim()) : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parseDuration(s: string): number {
|
|
238
|
+
const match = s.match(/^(\d+)(s|m|h|d)$/);
|
|
239
|
+
if (!match) {
|
|
240
|
+
console.error(`Invalid duration: ${s} (use e.g. 5m, 1h, 2d)`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const n = parseInt(match[1]!, 10);
|
|
244
|
+
switch (match[2]) {
|
|
245
|
+
case "s": return n * 1000;
|
|
246
|
+
case "m": return n * 60_000;
|
|
247
|
+
case "h": return n * 3_600_000;
|
|
248
|
+
case "d": return n * 86_400_000;
|
|
249
|
+
default: return n * 60_000;
|
|
250
|
+
}
|
|
251
|
+
}
|
package/runtime/cli.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { createContext, hasFlag } from "./cli/context";
|
|
4
|
+
|
|
5
|
+
const ctx = createContext(process.argv);
|
|
6
|
+
const args = ctx.args;
|
|
7
|
+
|
|
8
|
+
// Strip global flags to find the command
|
|
9
|
+
const globalFlags = ["--config", "-c", "--env", "-e"];
|
|
10
|
+
const commandArgs: string[] = [];
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
if (globalFlags.includes(args[i]!)) {
|
|
13
|
+
i++; // skip flag value
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
commandArgs.push(args[i]!);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const command = commandArgs[0];
|
|
20
|
+
const subcommand = commandArgs[1];
|
|
21
|
+
|
|
22
|
+
if (!command || hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
23
|
+
printHelp();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
switch (command) {
|
|
28
|
+
case "dev": {
|
|
29
|
+
const mod = await import("./cli/dev");
|
|
30
|
+
await mod.run(ctx);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "d1": {
|
|
34
|
+
const mod = await import("./cli/d1");
|
|
35
|
+
await mod.run(ctx, commandArgs.slice(1));
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "r2": {
|
|
39
|
+
const mod = await import("./cli/r2");
|
|
40
|
+
await mod.run(ctx, commandArgs.slice(1));
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "kv": {
|
|
44
|
+
const mod = await import("./cli/kv");
|
|
45
|
+
await mod.run(ctx, commandArgs.slice(1));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "queues": {
|
|
49
|
+
const mod = await import("./cli/queues");
|
|
50
|
+
await mod.run(ctx, commandArgs.slice(1));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "cache": {
|
|
54
|
+
const mod = await import("./cli/cache");
|
|
55
|
+
await mod.run(ctx, commandArgs.slice(1));
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "trace": {
|
|
59
|
+
const mod = await import("./cli/traces");
|
|
60
|
+
await mod.run(ctx, commandArgs.slice(1));
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
console.error(`Unknown command: ${command}`);
|
|
65
|
+
printHelp();
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printHelp() {
|
|
70
|
+
console.log(`
|
|
71
|
+
bunflare — local Cloudflare Worker dev tools
|
|
72
|
+
|
|
73
|
+
Usage: bunflare <command> [options]
|
|
74
|
+
|
|
75
|
+
Commands:
|
|
76
|
+
dev Start local dev server
|
|
77
|
+
d1 list List D1 databases
|
|
78
|
+
d1 execute <db> --command Execute SQL on a D1 database
|
|
79
|
+
d1 migrations apply [db] Apply D1 migrations
|
|
80
|
+
r2 object list [bucket] List R2 objects (bucket/prefix)
|
|
81
|
+
r2 object get <bucket/key> Get an R2 object
|
|
82
|
+
r2 object put <bucket/key> -f Upload a file to R2
|
|
83
|
+
r2 object delete <bucket/key> Delete an R2 object
|
|
84
|
+
kv key list List KV keys
|
|
85
|
+
kv key get <key> Get a KV value
|
|
86
|
+
kv key put <key> <value> Put a KV value
|
|
87
|
+
kv key delete <key> Delete a KV key
|
|
88
|
+
queues list List queues
|
|
89
|
+
queues message list <queue> List queue messages
|
|
90
|
+
queues message send <queue> Send a message to a queue
|
|
91
|
+
queues message purge <queue> Purge queue messages
|
|
92
|
+
cache list List cache names
|
|
93
|
+
cache purge [--name CACHE] Purge cache entries
|
|
94
|
+
trace list [options] List traces (--limit, --since, --search, --cursor)
|
|
95
|
+
trace get <traceId> Get trace detail as JSON
|
|
96
|
+
|
|
97
|
+
Global flags:
|
|
98
|
+
--config, -c <path> Path to wrangler config file
|
|
99
|
+
--env, -e <name> Environment name
|
|
100
|
+
--help, -h Show this help
|
|
101
|
+
`.trim());
|
|
102
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse as parseTOML } from "smol-toml";
|
|
4
|
+
import type { WorkflowLimits } from "./bindings/workflow";
|
|
5
|
+
|
|
6
|
+
export interface WranglerConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
main: string;
|
|
9
|
+
compatibility_date?: string;
|
|
10
|
+
compatibility_flags?: string[];
|
|
11
|
+
kv_namespaces?: { binding: string; id: string }[];
|
|
12
|
+
r2_buckets?: { binding: string; bucket_name: string }[];
|
|
13
|
+
durable_objects?: {
|
|
14
|
+
bindings: { name: string; class_name: string }[];
|
|
15
|
+
};
|
|
16
|
+
workflows?: { name: string; binding: string; class_name: string; limits?: Partial<WorkflowLimits> }[];
|
|
17
|
+
d1_databases?: { binding: string; database_name: string; database_id: string; migrations_dir?: string }[];
|
|
18
|
+
queues?: {
|
|
19
|
+
producers?: { binding: string; queue: string; delivery_delay?: number }[];
|
|
20
|
+
consumers?: { queue: string; max_batch_size?: number; max_batch_timeout?: number; max_retries?: number; dead_letter_queue?: string }[];
|
|
21
|
+
};
|
|
22
|
+
send_email?: {
|
|
23
|
+
name: string;
|
|
24
|
+
destination_address?: string;
|
|
25
|
+
allowed_destination_addresses?: string[];
|
|
26
|
+
}[];
|
|
27
|
+
ai?: { binding: string };
|
|
28
|
+
hyperdrive?: {
|
|
29
|
+
binding: string;
|
|
30
|
+
id: string;
|
|
31
|
+
localConnectionString?: string;
|
|
32
|
+
}[];
|
|
33
|
+
services?: { binding: string; service: string; entrypoint?: string }[];
|
|
34
|
+
triggers?: { crons?: string[] };
|
|
35
|
+
vars?: Record<string, string>;
|
|
36
|
+
assets?: {
|
|
37
|
+
directory: string;
|
|
38
|
+
binding?: string;
|
|
39
|
+
html_handling?: "none" | "auto-trailing-slash" | "force-trailing-slash" | "drop-trailing-slash";
|
|
40
|
+
not_found_handling?: "none" | "404-page" | "single-page-application";
|
|
41
|
+
run_worker_first?: boolean | string[];
|
|
42
|
+
};
|
|
43
|
+
images?: {
|
|
44
|
+
binding: string;
|
|
45
|
+
};
|
|
46
|
+
containers?: {
|
|
47
|
+
class_name: string;
|
|
48
|
+
image: string;
|
|
49
|
+
max_instances?: number;
|
|
50
|
+
instance_type?: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
}[];
|
|
53
|
+
analytics_engine_datasets?: { binding: string; dataset?: string }[];
|
|
54
|
+
browser?: { binding: string };
|
|
55
|
+
version_metadata?: { binding: string };
|
|
56
|
+
migrations?: { tag: string; new_classes?: string[]; new_sqlite_classes?: string[] }[];
|
|
57
|
+
env?: Record<string, Partial<Omit<WranglerConfig, "env">>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load config from an explicit path (JSON/JSONC/TOML).
|
|
62
|
+
*/
|
|
63
|
+
export async function loadConfig(path: string, envName?: string): Promise<WranglerConfig> {
|
|
64
|
+
const raw = await Bun.file(path).text();
|
|
65
|
+
let config: WranglerConfig;
|
|
66
|
+
if (path.endsWith(".toml")) {
|
|
67
|
+
config = parseTOML(raw) as unknown as WranglerConfig;
|
|
68
|
+
} else {
|
|
69
|
+
// JSON or JSONC — strip single-line comments (// ...) outside strings
|
|
70
|
+
config = JSON.parse(stripJsoncComments(raw));
|
|
71
|
+
}
|
|
72
|
+
return applyEnvOverrides(config, envName);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Auto-detect config file in a directory. Tries wrangler.jsonc, wrangler.json, wrangler.toml.
|
|
77
|
+
*/
|
|
78
|
+
export async function autoLoadConfig(baseDir: string, envName?: string): Promise<WranglerConfig> {
|
|
79
|
+
const candidates = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
|
|
80
|
+
for (const name of candidates) {
|
|
81
|
+
const fullPath = join(baseDir, name);
|
|
82
|
+
if (existsSync(fullPath)) {
|
|
83
|
+
return loadConfig(fullPath, envName);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`No wrangler config found in ${baseDir} (tried: ${candidates.join(", ")})`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Merge environment-specific overrides into the base config.
|
|
91
|
+
* Environment sections can override: vars, bindings, routes, triggers, etc.
|
|
92
|
+
*/
|
|
93
|
+
function applyEnvOverrides(config: WranglerConfig, envName?: string): WranglerConfig {
|
|
94
|
+
if (!envName || !config.env) return config;
|
|
95
|
+
const envConfig = config.env[envName];
|
|
96
|
+
if (!envConfig) {
|
|
97
|
+
throw new Error(`Environment "${envName}" not found in config. Available: ${Object.keys(config.env).join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
// Shallow merge: env-specific values override top-level ones
|
|
100
|
+
const { env: _env, ...base } = config;
|
|
101
|
+
const merged = { ...base };
|
|
102
|
+
for (const [key, value] of Object.entries(envConfig)) {
|
|
103
|
+
if (value !== undefined) {
|
|
104
|
+
(merged as Record<string, unknown>)[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return merged;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── JSONC Comment Stripping ───────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function stripJsoncComments(input: string): string {
|
|
113
|
+
let result = "";
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < input.length) {
|
|
116
|
+
// String literal — copy as-is
|
|
117
|
+
if (input[i] === '"') {
|
|
118
|
+
result += '"';
|
|
119
|
+
i++;
|
|
120
|
+
while (i < input.length && input[i] !== '"') {
|
|
121
|
+
if (input[i] === "\\") {
|
|
122
|
+
result += input[i]! + (input[i + 1] ?? "");
|
|
123
|
+
i += 2;
|
|
124
|
+
} else {
|
|
125
|
+
result += input[i]!;
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (i < input.length) { result += '"'; i++; }
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Single-line comment
|
|
133
|
+
if (input[i] === "/" && input[i + 1] === "/") {
|
|
134
|
+
while (i < input.length && input[i] !== "\n") i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Block comment
|
|
138
|
+
if (input[i] === "/" && input[i + 1] === "*") {
|
|
139
|
+
i += 2;
|
|
140
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
|
|
141
|
+
i += 2;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
result += input[i]!;
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply D1 migrations for all databases defined in wrangler config.
|
|
3
|
+
*
|
|
4
|
+
* Usage: bun runtime/d1-migrate.ts [--config path/to/wrangler.jsonc] [--env envName]
|
|
5
|
+
*
|
|
6
|
+
* Equivalent to: wrangler d1 migrations apply <db> --local
|
|
7
|
+
*
|
|
8
|
+
* This is a backward-compatible wrapper around the CLI migration logic.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolve, join } from "node:path";
|
|
12
|
+
import { autoLoadConfig, loadConfig } from "./config";
|
|
13
|
+
import { applyMigrations } from "./cli/d1";
|
|
14
|
+
|
|
15
|
+
// Parse CLI flags
|
|
16
|
+
function parseFlag(name: string): string | undefined {
|
|
17
|
+
const idx = process.argv.indexOf(name);
|
|
18
|
+
return idx !== -1 ? process.argv[idx + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configPath = parseFlag("--config") ?? parseFlag("-c");
|
|
22
|
+
const envName = parseFlag("--env") ?? parseFlag("-e");
|
|
23
|
+
const baseDir = process.cwd();
|
|
24
|
+
|
|
25
|
+
const config = configPath
|
|
26
|
+
? await loadConfig(resolve(baseDir, configPath), envName)
|
|
27
|
+
: await autoLoadConfig(baseDir, envName);
|
|
28
|
+
|
|
29
|
+
const databases = config.d1_databases ?? [];
|
|
30
|
+
|
|
31
|
+
if (databases.length === 0) {
|
|
32
|
+
console.log("[d1-migrate] No D1 databases configured.");
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dataDir = join(baseDir, ".bunflare");
|
|
37
|
+
await applyMigrations(databases, dataDir);
|