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.
Files changed (147) hide show
  1. package/README.md +15 -0
  2. package/package.json +51 -0
  3. package/runtime/bindings/ai.ts +132 -0
  4. package/runtime/bindings/analytics-engine.ts +96 -0
  5. package/runtime/bindings/browser.ts +64 -0
  6. package/runtime/bindings/cache.ts +179 -0
  7. package/runtime/bindings/cf-streams.ts +56 -0
  8. package/runtime/bindings/container-docker.ts +225 -0
  9. package/runtime/bindings/container.ts +662 -0
  10. package/runtime/bindings/crypto-extras.ts +89 -0
  11. package/runtime/bindings/d1.ts +315 -0
  12. package/runtime/bindings/do-executor-inprocess.ts +140 -0
  13. package/runtime/bindings/do-executor-worker.ts +368 -0
  14. package/runtime/bindings/do-executor.ts +45 -0
  15. package/runtime/bindings/do-websocket-bridge.ts +70 -0
  16. package/runtime/bindings/do-worker-entry.ts +220 -0
  17. package/runtime/bindings/do-worker-env.ts +74 -0
  18. package/runtime/bindings/durable-object.ts +992 -0
  19. package/runtime/bindings/email.ts +180 -0
  20. package/runtime/bindings/html-rewriter.ts +84 -0
  21. package/runtime/bindings/hyperdrive.ts +130 -0
  22. package/runtime/bindings/images.ts +381 -0
  23. package/runtime/bindings/kv.ts +359 -0
  24. package/runtime/bindings/queue.ts +507 -0
  25. package/runtime/bindings/r2.ts +759 -0
  26. package/runtime/bindings/rpc-stub.ts +267 -0
  27. package/runtime/bindings/scheduled.ts +172 -0
  28. package/runtime/bindings/service-binding.ts +217 -0
  29. package/runtime/bindings/static-assets.ts +481 -0
  30. package/runtime/bindings/websocket-pair.ts +182 -0
  31. package/runtime/bindings/workflow.ts +858 -0
  32. package/runtime/bunflare-config.ts +56 -0
  33. package/runtime/cli/cache.ts +39 -0
  34. package/runtime/cli/context.ts +105 -0
  35. package/runtime/cli/d1.ts +163 -0
  36. package/runtime/cli/dev.ts +392 -0
  37. package/runtime/cli/kv.ts +84 -0
  38. package/runtime/cli/queues.ts +109 -0
  39. package/runtime/cli/r2.ts +140 -0
  40. package/runtime/cli/traces.ts +251 -0
  41. package/runtime/cli.ts +102 -0
  42. package/runtime/config.ts +148 -0
  43. package/runtime/d1-migrate.ts +37 -0
  44. package/runtime/dashboard/api.ts +174 -0
  45. package/runtime/dashboard/app.tsx +220 -0
  46. package/runtime/dashboard/components/breadcrumb.tsx +16 -0
  47. package/runtime/dashboard/components/buttons.tsx +13 -0
  48. package/runtime/dashboard/components/code-block.tsx +5 -0
  49. package/runtime/dashboard/components/detail-field.tsx +8 -0
  50. package/runtime/dashboard/components/empty-state.tsx +8 -0
  51. package/runtime/dashboard/components/filter-input.tsx +11 -0
  52. package/runtime/dashboard/components/index.ts +16 -0
  53. package/runtime/dashboard/components/key-value-table.tsx +23 -0
  54. package/runtime/dashboard/components/modal.tsx +23 -0
  55. package/runtime/dashboard/components/page-header.tsx +11 -0
  56. package/runtime/dashboard/components/pill-button.tsx +14 -0
  57. package/runtime/dashboard/components/refresh-button.tsx +7 -0
  58. package/runtime/dashboard/components/service-info.tsx +45 -0
  59. package/runtime/dashboard/components/status-badge.tsx +7 -0
  60. package/runtime/dashboard/components/table-link.tsx +5 -0
  61. package/runtime/dashboard/components/table.tsx +26 -0
  62. package/runtime/dashboard/components.tsx +19 -0
  63. package/runtime/dashboard/index.html +23 -0
  64. package/runtime/dashboard/lib.ts +45 -0
  65. package/runtime/dashboard/rpc/client.ts +20 -0
  66. package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
  67. package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
  68. package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
  69. package/runtime/dashboard/rpc/handlers/config.ts +137 -0
  70. package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
  71. package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
  72. package/runtime/dashboard/rpc/handlers/do.ts +117 -0
  73. package/runtime/dashboard/rpc/handlers/email.ts +82 -0
  74. package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
  75. package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
  76. package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
  77. package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
  78. package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
  79. package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
  80. package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
  81. package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
  82. package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
  83. package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
  84. package/runtime/dashboard/rpc/hooks.ts +132 -0
  85. package/runtime/dashboard/rpc/server.ts +70 -0
  86. package/runtime/dashboard/rpc/types.ts +396 -0
  87. package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
  88. package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
  89. package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
  90. package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
  91. package/runtime/dashboard/sql-browser/hooks.ts +137 -0
  92. package/runtime/dashboard/sql-browser/index.ts +4 -0
  93. package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
  94. package/runtime/dashboard/sql-browser/modals.tsx +116 -0
  95. package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
  96. package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
  97. package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
  98. package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
  99. package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
  100. package/runtime/dashboard/sql-browser/types.ts +61 -0
  101. package/runtime/dashboard/sql-browser/utils.ts +167 -0
  102. package/runtime/dashboard/style.css +177 -0
  103. package/runtime/dashboard/views/ai.tsx +152 -0
  104. package/runtime/dashboard/views/analytics-engine.tsx +169 -0
  105. package/runtime/dashboard/views/cache.tsx +93 -0
  106. package/runtime/dashboard/views/containers.tsx +197 -0
  107. package/runtime/dashboard/views/d1.tsx +81 -0
  108. package/runtime/dashboard/views/do.tsx +168 -0
  109. package/runtime/dashboard/views/email.tsx +235 -0
  110. package/runtime/dashboard/views/errors.tsx +558 -0
  111. package/runtime/dashboard/views/home.tsx +287 -0
  112. package/runtime/dashboard/views/kv.tsx +273 -0
  113. package/runtime/dashboard/views/queue.tsx +193 -0
  114. package/runtime/dashboard/views/r2.tsx +202 -0
  115. package/runtime/dashboard/views/scheduled.tsx +89 -0
  116. package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
  117. package/runtime/dashboard/views/traces.tsx +768 -0
  118. package/runtime/dashboard/views/workers.tsx +55 -0
  119. package/runtime/dashboard/views/workflows.tsx +473 -0
  120. package/runtime/db.ts +258 -0
  121. package/runtime/env.ts +362 -0
  122. package/runtime/error-page/app.tsx +394 -0
  123. package/runtime/error-page/build.ts +269 -0
  124. package/runtime/error-page/index.html +16 -0
  125. package/runtime/error-page/style.css +31 -0
  126. package/runtime/execution-context.ts +18 -0
  127. package/runtime/file-watcher.ts +57 -0
  128. package/runtime/generation-manager.ts +230 -0
  129. package/runtime/generation.ts +411 -0
  130. package/runtime/plugin.ts +292 -0
  131. package/runtime/request-cf.ts +28 -0
  132. package/runtime/rpc-validate.ts +154 -0
  133. package/runtime/tracing/context.ts +40 -0
  134. package/runtime/tracing/db.ts +73 -0
  135. package/runtime/tracing/frames.ts +75 -0
  136. package/runtime/tracing/instrument.ts +186 -0
  137. package/runtime/tracing/span.ts +138 -0
  138. package/runtime/tracing/store.ts +499 -0
  139. package/runtime/tracing/types.ts +47 -0
  140. package/runtime/vite-plugin/config-plugin.ts +68 -0
  141. package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
  142. package/runtime/vite-plugin/dist/index.mjs +52333 -0
  143. package/runtime/vite-plugin/globals-plugin.ts +94 -0
  144. package/runtime/vite-plugin/index.ts +43 -0
  145. package/runtime/vite-plugin/modules-plugin.ts +88 -0
  146. package/runtime/vite-plugin/react-router-plugin.ts +95 -0
  147. 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);