flingit 0.0.9 → 0.0.11

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 (153) hide show
  1. package/dist/cli/commands/admin.d.ts.map +1 -1
  2. package/dist/cli/commands/admin.js +29 -16
  3. package/dist/cli/commands/admin.js.map +1 -1
  4. package/dist/cli/commands/cron.d.ts.map +1 -1
  5. package/dist/cli/commands/cron.js +7 -3
  6. package/dist/cli/commands/cron.js.map +1 -1
  7. package/dist/cli/commands/db.d.ts.map +1 -1
  8. package/dist/cli/commands/db.js +5 -2
  9. package/dist/cli/commands/db.js.map +1 -1
  10. package/dist/cli/commands/feedback.d.ts.map +1 -1
  11. package/dist/cli/commands/feedback.js +6 -1
  12. package/dist/cli/commands/feedback.js.map +1 -1
  13. package/dist/cli/commands/init.d.ts.map +1 -1
  14. package/dist/cli/commands/init.js +5 -20
  15. package/dist/cli/commands/init.js.map +1 -1
  16. package/dist/cli/commands/launch.d.ts.map +1 -1
  17. package/dist/cli/commands/launch.js +5 -59
  18. package/dist/cli/commands/launch.js.map +1 -1
  19. package/dist/cli/commands/login.d.ts.map +1 -1
  20. package/dist/cli/commands/login.js +7 -2
  21. package/dist/cli/commands/login.js.map +1 -1
  22. package/dist/cli/commands/logs.d.ts.map +1 -1
  23. package/dist/cli/commands/logs.js +3 -1
  24. package/dist/cli/commands/logs.js.map +1 -1
  25. package/dist/cli/commands/onboard.js +1 -1
  26. package/dist/cli/commands/plugin.d.ts +8 -0
  27. package/dist/cli/commands/plugin.d.ts.map +1 -0
  28. package/dist/cli/commands/plugin.js +407 -0
  29. package/dist/cli/commands/plugin.js.map +1 -0
  30. package/dist/cli/commands/project.d.ts.map +1 -1
  31. package/dist/cli/commands/project.js +3 -1
  32. package/dist/cli/commands/project.js.map +1 -1
  33. package/dist/cli/commands/push.d.ts.map +1 -1
  34. package/dist/cli/commands/push.js +128 -12
  35. package/dist/cli/commands/push.js.map +1 -1
  36. package/dist/cli/commands/register.d.ts +25 -0
  37. package/dist/cli/commands/register.d.ts.map +1 -1
  38. package/dist/cli/commands/register.js +30 -13
  39. package/dist/cli/commands/register.js.map +1 -1
  40. package/dist/cli/commands/secret.d.ts +9 -0
  41. package/dist/cli/commands/secret.d.ts.map +1 -1
  42. package/dist/cli/commands/secret.js +40 -50
  43. package/dist/cli/commands/secret.js.map +1 -1
  44. package/dist/cli/commands/storage.d.ts +10 -0
  45. package/dist/cli/commands/storage.d.ts.map +1 -0
  46. package/dist/cli/commands/storage.js +459 -0
  47. package/dist/cli/commands/storage.js.map +1 -0
  48. package/dist/cli/commands/tun.d.ts.map +1 -1
  49. package/dist/cli/commands/tun.js +3 -2
  50. package/dist/cli/commands/tun.js.map +1 -1
  51. package/dist/cli/deploy/bundler.d.ts +15 -0
  52. package/dist/cli/deploy/bundler.d.ts.map +1 -1
  53. package/dist/cli/deploy/bundler.js +86 -2
  54. package/dist/cli/deploy/bundler.js.map +1 -1
  55. package/dist/cli/index.js +4 -0
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/utils/config.d.ts +12 -0
  58. package/dist/cli/utils/config.d.ts.map +1 -1
  59. package/dist/cli/utils/config.js +23 -3
  60. package/dist/cli/utils/config.js.map +1 -1
  61. package/dist/cli/utils/environment.d.ts +12 -0
  62. package/dist/cli/utils/environment.d.ts.map +1 -1
  63. package/dist/cli/utils/environment.js +40 -6
  64. package/dist/cli/utils/environment.js.map +1 -1
  65. package/dist/cli/utils/launch-io-impl.d.ts +13 -0
  66. package/dist/cli/utils/launch-io-impl.d.ts.map +1 -0
  67. package/dist/cli/utils/launch-io-impl.js +51 -0
  68. package/dist/cli/utils/launch-io-impl.js.map +1 -0
  69. package/dist/cli/utils/launch-io.d.ts +42 -0
  70. package/dist/cli/utils/launch-io.d.ts.map +1 -0
  71. package/dist/cli/utils/launch-io.js +8 -0
  72. package/dist/cli/utils/launch-io.js.map +1 -0
  73. package/dist/cli/utils/project-name.d.ts +17 -0
  74. package/dist/cli/utils/project-name.d.ts.map +1 -0
  75. package/dist/cli/utils/project-name.js +33 -0
  76. package/dist/cli/utils/project-name.js.map +1 -0
  77. package/dist/cli/utils/prompt-new-project.d.ts +12 -0
  78. package/dist/cli/utils/prompt-new-project.d.ts.map +1 -0
  79. package/dist/cli/utils/prompt-new-project.js +66 -0
  80. package/dist/cli/utils/prompt-new-project.js.map +1 -0
  81. package/dist/cli/utils/version-check.d.ts +30 -0
  82. package/dist/cli/utils/version-check.d.ts.map +1 -0
  83. package/dist/cli/utils/version-check.js +43 -0
  84. package/dist/cli/utils/version-check.js.map +1 -0
  85. package/dist/client/assets/index-BqVrS7t9.js +40 -0
  86. package/dist/client/assets/index-DSLUsCtO.css +1 -0
  87. package/dist/client/index.html +14 -0
  88. package/dist/client/vite.svg +1 -0
  89. package/dist/index.d.ts +3 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +2 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/runtime/db.d.ts +5 -0
  94. package/dist/runtime/db.d.ts.map +1 -1
  95. package/dist/runtime/db.js +6 -1
  96. package/dist/runtime/db.js.map +1 -1
  97. package/dist/runtime/discord.d.ts +171 -0
  98. package/dist/runtime/discord.d.ts.map +1 -0
  99. package/dist/runtime/discord.js +192 -0
  100. package/dist/runtime/discord.js.map +1 -0
  101. package/dist/runtime/secrets.d.ts +28 -0
  102. package/dist/runtime/secrets.d.ts.map +1 -1
  103. package/dist/runtime/secrets.js +77 -20
  104. package/dist/runtime/secrets.js.map +1 -1
  105. package/dist/runtime/storage.d.ts +29 -0
  106. package/dist/runtime/storage.d.ts.map +1 -0
  107. package/dist/runtime/storage.js +456 -0
  108. package/dist/runtime/storage.js.map +1 -0
  109. package/dist/shared/constants.d.ts +12 -0
  110. package/dist/shared/constants.d.ts.map +1 -0
  111. package/dist/shared/constants.js +12 -0
  112. package/dist/shared/constants.js.map +1 -0
  113. package/dist/shared/discord-types.d.ts +130 -0
  114. package/dist/shared/discord-types.d.ts.map +1 -0
  115. package/dist/shared/discord-types.js +8 -0
  116. package/dist/shared/discord-types.js.map +1 -0
  117. package/dist/shared/random-code.d.ts +6 -0
  118. package/dist/shared/random-code.d.ts.map +1 -0
  119. package/dist/shared/random-code.js +23 -0
  120. package/dist/shared/random-code.js.map +1 -0
  121. package/dist/types/storage.d.ts +154 -0
  122. package/dist/types/storage.d.ts.map +1 -0
  123. package/dist/types/storage.js +8 -0
  124. package/dist/types/storage.js.map +1 -0
  125. package/dist/worker-runtime/discord.d.ts +61 -0
  126. package/dist/worker-runtime/discord.d.ts.map +1 -0
  127. package/dist/worker-runtime/discord.js +181 -0
  128. package/dist/worker-runtime/discord.js.map +1 -0
  129. package/dist/worker-runtime/index.d.ts +21 -1
  130. package/dist/worker-runtime/index.d.ts.map +1 -1
  131. package/dist/worker-runtime/index.js +102 -0
  132. package/dist/worker-runtime/index.js.map +1 -1
  133. package/package.json +9 -2
  134. package/templates/default/CLAUDE.md +65 -11
  135. package/templates/default/skills/discord/SKILL.md +328 -0
  136. package/templates/default/skills/fling/API.md +208 -0
  137. package/templates/default/skills/fling/SKILL.md +51 -8
  138. package/dist/cli/deploy/worker-entry.d.ts +0 -14
  139. package/dist/cli/deploy/worker-entry.d.ts.map +0 -1
  140. package/dist/cli/deploy/worker-entry.js +0 -60
  141. package/dist/cli/deploy/worker-entry.js.map +0 -1
  142. package/dist/cli/deploy/wrangler-config.d.ts +0 -20
  143. package/dist/cli/deploy/wrangler-config.d.ts.map +0 -1
  144. package/dist/cli/deploy/wrangler-config.js +0 -54
  145. package/dist/cli/deploy/wrangler-config.js.map +0 -1
  146. package/dist/cli/loaders/register-wasm.d.ts +0 -12
  147. package/dist/cli/loaders/register-wasm.d.ts.map +0 -1
  148. package/dist/cli/loaders/register-wasm.js +0 -19
  149. package/dist/cli/loaders/register-wasm.js.map +0 -1
  150. package/dist/cli/loaders/wasm-hooks.d.ts +0 -61
  151. package/dist/cli/loaders/wasm-hooks.d.ts.map +0 -1
  152. package/dist/cli/loaders/wasm-hooks.js +0 -63
  153. package/dist/cli/loaders/wasm-hooks.js.map +0 -1
@@ -50,7 +50,7 @@ Just edit and save - changes appear immediately.
50
50
  All primitives are imported from `"flingit"` in the backend:
51
51
 
52
52
  ```typescript
53
- import { app, db, migrate, secrets, cron } from "flingit";
53
+ import { app, db, migrate, secrets, cron, storage } from "flingit";
54
54
  ```
55
55
 
56
56
  ### HTTP Routes (Hono)
@@ -129,6 +129,49 @@ npm exec fling cron history <name> # View invocation history
129
129
  npm exec fling cron trigger <name> # Manually trigger a job
130
130
  ```
131
131
 
132
+ ### Storage (R2)
133
+
134
+ Store and retrieve files. Uses local filesystem in development, Cloudflare R2 in production.
135
+
136
+ ```typescript
137
+ import { storage } from "flingit";
138
+
139
+ // Store objects
140
+ await storage.put("images/logo.png", imageBuffer, { contentType: "image/png" });
141
+ await storage.put("data/config.json", JSON.stringify(config));
142
+
143
+ // Retrieve objects
144
+ const file = await storage.get("images/logo.png");
145
+ if (file) {
146
+ const buffer = await file.arrayBuffer();
147
+ const text = await file.text();
148
+ const json = await file.json<ConfigType>();
149
+ // Or stream: file.body (ReadableStream)
150
+ }
151
+
152
+ // Check existence without downloading
153
+ const meta = await storage.head("images/logo.png");
154
+
155
+ // Delete objects
156
+ await storage.delete("old-file.txt");
157
+ await storage.delete(["file1.txt", "file2.txt"]); // Batch delete
158
+
159
+ // List objects
160
+ const result = await storage.list({ prefix: "images/", limit: 100 });
161
+ for (const obj of result.objects) {
162
+ console.log(`${obj.key}: ${obj.size} bytes`);
163
+ }
164
+ ```
165
+
166
+ Manage storage with CLI:
167
+ ```bash
168
+ npm exec fling storage list # List local storage objects
169
+ npm exec fling storage put key file.txt # Upload file to storage
170
+ npm exec fling storage get key output # Download object to file
171
+ npm exec fling storage delete key --yes # Delete object
172
+ npm exec fling storage info # Show storage stats
173
+ ```
174
+
132
175
  ## React Frontend
133
176
 
134
177
  The frontend is a standard React + Vite setup. Call the backend API:
@@ -155,22 +198,27 @@ npm exec fling db sql "SELECT * FROM users" # Query local SQLite
155
198
  npm exec fling secret set K=V # Set local secret
156
199
  npm exec fling secret list # List local secrets
157
200
  npm exec fling logs # View local logs
201
+ npm exec fling storage list # List storage objects
202
+ npm exec fling storage put K F # Upload file F as key K
203
+ npm exec fling storage get K O # Download key K to file O
158
204
  npm exec fling push # Build and deploy to production
159
205
  ```
160
206
 
161
207
  ### Local vs Production (`--prod`)
162
208
 
163
- Most CLI commands operate on the **local** environment by default. Add `--prod` for production (logs and db only):
209
+ Most CLI commands operate on the **local** environment by default. Add `--prod` for production:
164
210
 
165
211
  ```bash
166
212
  # Local (default)
167
213
  npm exec fling secret list # Local secrets
168
214
  npm exec fling logs # Local logs
169
215
  npm exec fling db sql "SELECT 1" # Local SQLite
216
+ npm exec fling storage list # Local storage
170
217
 
171
218
  # Production (requires login)
172
219
  npm exec fling -- --prod logs # Deployed logs
173
220
  npm exec fling -- --prod db sql "SELECT 1" # Deployed D1
221
+ npm exec fling -- --prod storage list # R2 storage
174
222
  ```
175
223
 
176
224
  **Note:** Production logs have a delay of ~10 seconds or more before they appear.
@@ -198,8 +246,8 @@ npm exec fling -- --prod db sql "SELECT 1" # Deployed D1
198
246
  Be aware of these constraints when building:
199
247
 
200
248
  ### Feature Scope
201
- - **Supported:** Frontend (React), backend (Hono API), database (SQLite/D1), cron jobs
202
- - **Not yet supported:** File/blob storage, websockets
249
+ - **Supported:** Frontend (React), backend (Hono API), database (SQLite/D1), cron jobs, file storage (R2), Discord integration
250
+ - **Not yet supported:** Websockets
203
251
  - If the user needs unsupported features, encourage them to send feedback via `npm exec fling feedback`
204
252
 
205
253
  ### Backend Memory (128MB limit)
@@ -216,11 +264,16 @@ Be aware of these constraints when building:
216
264
 
217
265
  ### Backend Assets (Images, Fonts, etc.)
218
266
 
219
- If the backend needs to return or process an asset (image, icon, font), keep it small and embed it directly in the code as base64:
267
+ If the backend needs to return or process an asset (image, icon, font), keep it small and store the base64 data in a separate file to keep your main code clean:
268
+
269
+ ```typescript
270
+ // src/worker/assets/favicon.ts - separate file for the asset
271
+ export const FAVICON_BASE64 = "iVBORw0KGgo..."; // base64-encoded PNG
272
+ ```
220
273
 
221
274
  ```typescript
222
- // Small assets should be base64 encoded directly in the code
223
- const FAVICON_BASE64 = "iVBORw0KGgo..."; // base64-encoded PNG
275
+ // src/worker/index.ts - import and use the asset
276
+ import { FAVICON_BASE64 } from "./assets/favicon";
224
277
 
225
278
  app.get("/favicon.ico", (c) => {
226
279
  const buffer = Uint8Array.from(atob(FAVICON_BASE64), c => c.charCodeAt(0));
@@ -230,10 +283,10 @@ app.get("/favicon.ico", (c) => {
230
283
  });
231
284
  ```
232
285
 
233
- This is necessary because:
234
- - Workers have limited memory (~128MB)
235
- - There's no built-in asset serving for backend code
236
- - Base64 keeps assets bundled with the code
286
+ This approach:
287
+ - Keeps the main code readable (no long base64 strings inline)
288
+ - Makes assets easy to find and update
289
+ - Base64 keeps assets bundled with the code (required for Workers)
237
290
 
238
291
  **For large assets:** Serve them from the frontend (`public/` folder) instead.
239
292
 
@@ -260,3 +313,4 @@ Slugs must be:
260
313
  - Globally unique across all Fling projects
261
314
 
262
315
  See `.claude/skills/fling/` for detailed API documentation.
316
+ See `.claude/skills/discord/` for Discord chatops integration (slash commands, messages, interactions).
@@ -0,0 +1,328 @@
1
+ # Discord Skill
2
+
3
+ Build Discord chatops bots with Fling. Add slash commands, send messages, handle interactions, and react to events — all from your Fling project.
4
+
5
+ ## Setup
6
+
7
+ Discord requires a one-time setup before use:
8
+
9
+ ```bash
10
+ npm exec fling plugin install discord # Connect Discord account (opens browser)
11
+ npm exec fling plugin discord add-server # Claim a server for your project
12
+ npm exec fling push # Deploy code + register slash commands
13
+ ```
14
+
15
+ Run these commands yourself — you have bash access.
16
+
17
+ ## Quick Reference
18
+
19
+ ```typescript
20
+ import { app } from "flingit";
21
+ import { discord } from "flingit/plugin/discord";
22
+
23
+ // 1. Define slash commands (synced to Discord on `fling push`)
24
+ export const commands = discord.defineCommands([
25
+ { name: "ping", description: "Check bot latency" },
26
+ {
27
+ name: "deploy",
28
+ description: "Deploy a branch",
29
+ options: [
30
+ { name: "branch", type: "string", description: "Branch name", required: true }
31
+ ]
32
+ }
33
+ ]);
34
+
35
+ // 2. Handle slash commands on POST /discord
36
+ app.post("/discord", async (c) => {
37
+ const interaction = await discord.verifyInteraction(c);
38
+
39
+ if (interaction.data?.name === "ping") {
40
+ await discord.replyToInteraction(interaction, {
41
+ content: "Pong!",
42
+ ephemeral: true // Only visible to the user who ran the command
43
+ });
44
+ }
45
+
46
+ if (interaction.data?.name === "deploy") {
47
+ const branch = interaction.data.options?.[0]?.value as string;
48
+ await discord.replyToInteraction(interaction, {
49
+ content: `Deploying ${branch}...`
50
+ });
51
+
52
+ // Do work after the initial reply...
53
+ const result = await runDeploy(branch);
54
+
55
+ // Send a followup message (no 3-second limit)
56
+ await discord.sendFollowup(interaction, {
57
+ content: `Deployed ${branch}: ${result}`
58
+ });
59
+ }
60
+
61
+ return c.json({ ok: true });
62
+ });
63
+
64
+ // 3. Send messages proactively (e.g., from webhooks or cron)
65
+ app.post("/api/notify", async (c) => {
66
+ const { channelId, text } = await c.req.json();
67
+
68
+ await discord.sendMessage({
69
+ channelId,
70
+ content: text
71
+ });
72
+
73
+ return c.json({ sent: true });
74
+ });
75
+ ```
76
+
77
+ ## API Reference
78
+
79
+ All methods are imported from `"flingit/plugin/discord"`:
80
+
81
+ ```typescript
82
+ import { discord } from "flingit/plugin/discord";
83
+ ```
84
+
85
+ ### discord.defineCommands(commands)
86
+
87
+ Register slash commands. **Must be exported** as a top-level `export const commands` for `fling push` to detect them.
88
+
89
+ ```typescript
90
+ export const commands = discord.defineCommands([
91
+ { name: "status", description: "Show system status" },
92
+ {
93
+ name: "lookup",
94
+ description: "Look up a user",
95
+ options: [
96
+ { name: "email", type: "string", description: "User email", required: true },
97
+ { name: "verbose", type: "boolean", description: "Show full details" }
98
+ ]
99
+ }
100
+ ]);
101
+ ```
102
+
103
+ **Command names:** 1-32 chars, lowercase letters, numbers, and hyphens only.
104
+
105
+ **Option types:** `"string"`, `"integer"`, `"boolean"`, `"user"`, `"channel"`, `"role"`
106
+
107
+ Options can have predefined choices:
108
+
109
+ ```typescript
110
+ {
111
+ name: "env",
112
+ type: "string",
113
+ description: "Target environment",
114
+ required: true,
115
+ choices: [
116
+ { name: "Production", value: "prod" },
117
+ { name: "Staging", value: "staging" }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ### discord.verifyInteraction(c)
123
+
124
+ Parse an incoming Discord interaction from a Hono request context. Call this in your `POST /discord` handler.
125
+
126
+ ```typescript
127
+ app.post("/discord", async (c) => {
128
+ const interaction = await discord.verifyInteraction(c);
129
+
130
+ // interaction.data.name = command name
131
+ // interaction.data.options = command arguments
132
+ // interaction.member.user = who ran the command (in servers)
133
+ // interaction.guild_id = which server
134
+ // interaction.channel_id = which channel
135
+ });
136
+ ```
137
+
138
+ The platform verifies Discord's cryptographic signature before forwarding to your code — you don't need to handle that.
139
+
140
+ ### discord.replyToInteraction(interaction, options)
141
+
142
+ Reply to a slash command. **Must be called within 3 seconds** of receiving the interaction.
143
+
144
+ ```typescript
145
+ await discord.replyToInteraction(interaction, {
146
+ content: "Hello!", // Text content (max 2000 chars)
147
+ ephemeral: true, // Only visible to command user (optional)
148
+ embeds: [{ // Rich embeds (optional, max 10)
149
+ title: "Status",
150
+ description: "All systems operational",
151
+ color: 0x00ff00
152
+ }]
153
+ });
154
+ ```
155
+
156
+ ### discord.sendFollowup(interaction, options)
157
+
158
+ Send additional messages after the initial reply. No 3-second time limit.
159
+
160
+ ```typescript
161
+ await discord.replyToInteraction(interaction, { content: "Working on it..." });
162
+
163
+ // ... do async work ...
164
+
165
+ const msg = await discord.sendFollowup(interaction, {
166
+ content: "Done! Here are the results.",
167
+ embeds: [{ title: "Results", description: resultText }]
168
+ });
169
+ // Returns: { id, channelId, content }
170
+ ```
171
+
172
+ ### discord.sendMessage(options)
173
+
174
+ Send a message to any channel in a claimed server. Use this for notifications, alerts, or proactive messages.
175
+
176
+ ```typescript
177
+ const msg = await discord.sendMessage({
178
+ channelId: "123456789012345678",
179
+ content: "Deployment complete!",
180
+ embeds: [{
181
+ title: "Deploy Report",
182
+ description: "v2.1.0 is now live",
183
+ color: 0x00ff00,
184
+ fields: [
185
+ { name: "Duration", value: "42s", inline: true },
186
+ { name: "Status", value: "Success", inline: true }
187
+ ],
188
+ timestamp: new Date().toISOString()
189
+ }]
190
+ });
191
+ // Returns: { id, channelId, content }
192
+ ```
193
+
194
+ The channel must be in a server claimed by your project.
195
+
196
+ ### discord.editMessage(channelId, messageId, options)
197
+
198
+ Edit a previously sent message.
199
+
200
+ ```typescript
201
+ const msg = await discord.sendMessage({
202
+ channelId: channel,
203
+ content: "Deploying..."
204
+ });
205
+
206
+ // Later, update it
207
+ await discord.editMessage(channel, msg.id, {
208
+ content: "Deploy complete!",
209
+ embeds: [{ title: "Done", color: 0x00ff00 }]
210
+ });
211
+ ```
212
+
213
+ ### discord.addReaction(channelId, messageId, emoji)
214
+
215
+ Add an emoji reaction to a message.
216
+
217
+ ```typescript
218
+ await discord.addReaction(channelId, messageId, "✅");
219
+ await discord.addReaction(channelId, messageId, "🚀");
220
+ ```
221
+
222
+ ## Embeds
223
+
224
+ Rich embeds support these fields:
225
+
226
+ ```typescript
227
+ const embed: DiscordEmbed = {
228
+ title: "Title text",
229
+ description: "Body text (supports markdown)",
230
+ color: 0xff5733, // Hex color as number
231
+ url: "https://example.com", // Makes title a link
232
+ timestamp: new Date().toISOString(),
233
+ footer: { text: "Footer text", icon_url: "https://..." },
234
+ author: { name: "Author", url: "https://...", icon_url: "https://..." },
235
+ fields: [ // Up to 25 fields
236
+ { name: "Field 1", value: "Value 1", inline: true },
237
+ { name: "Field 2", value: "Value 2", inline: true }
238
+ ]
239
+ };
240
+ ```
241
+
242
+ Content max: 2000 characters. Embeds max: 10 per message.
243
+
244
+ ## Reading Command Options
245
+
246
+ Access option values from `interaction.data.options`:
247
+
248
+ ```typescript
249
+ app.post("/discord", async (c) => {
250
+ const interaction = await discord.verifyInteraction(c);
251
+
252
+ if (interaction.data?.name === "deploy") {
253
+ const options = interaction.data.options ?? [];
254
+ const branch = options.find(o => o.name === "branch")?.value as string;
255
+ const force = options.find(o => o.name === "force")?.value as boolean ?? false;
256
+
257
+ await discord.replyToInteraction(interaction, {
258
+ content: `Deploying ${branch}${force ? " (force)" : ""}...`
259
+ });
260
+ }
261
+
262
+ return c.json({ ok: true });
263
+ });
264
+ ```
265
+
266
+ ## Interaction Context
267
+
268
+ Access who ran the command and where:
269
+
270
+ ```typescript
271
+ app.post("/discord", async (c) => {
272
+ const interaction = await discord.verifyInteraction(c);
273
+
274
+ // Who ran it
275
+ const user = interaction.member?.user; // In servers
276
+ // user.id, user.username
277
+
278
+ // Where
279
+ const guildId = interaction.guild_id; // Server ID
280
+ const channelId = interaction.channel_id; // Channel ID
281
+
282
+ // Permissions
283
+ const roles = interaction.member?.roles; // Role IDs
284
+ const perms = interaction.member?.permissions; // Permission bitfield
285
+ });
286
+ ```
287
+
288
+ ## CLI Commands
289
+
290
+ ```bash
291
+ # Setup
292
+ npm exec fling plugin install discord # Connect Discord (OAuth)
293
+ npm exec fling plugin discord add-server # Claim a server
294
+ npm exec fling plugin discord remove-server # Release a server
295
+
296
+ # Status
297
+ npm exec fling plugin permissions discord # Show connection status + servers
298
+
299
+ # Deployment (registers slash commands automatically)
300
+ npm exec fling push
301
+
302
+ # Teardown
303
+ npm exec fling plugin remove discord # Disconnect, release all servers
304
+ ```
305
+
306
+ ## How It Works
307
+
308
+ 1. **`fling plugin install discord`** opens a browser for Discord OAuth. You authorize Fling to see your servers and add the bot.
309
+ 2. **`fling plugin discord add-server`** claims a Discord server for your project. Each server can only be claimed by one project.
310
+ 3. **`export const commands = discord.defineCommands([...])`** in your code defines slash commands. `fling push` extracts them and registers them with Discord in all claimed servers.
311
+ 4. When a user runs a slash command, Discord sends it to the platform, which routes it to your worker's `POST /discord` endpoint.
312
+ 5. Your code calls `discord.verifyInteraction(c)` to parse it and `discord.replyToInteraction()` to respond.
313
+
314
+ ## Important Constraints
315
+
316
+ 1. **Discord features only work in deployed workers** — They throw errors locally. Use `fling push` to deploy, then test in Discord.
317
+
318
+ 2. **Reply within 3 seconds** — `replyToInteraction()` must be called within 3 seconds. For slow operations, reply immediately with "Working..." then use `sendFollowup()` for the result.
319
+
320
+ 3. **Commands must be exported** — `fling push` looks for `export const commands = discord.defineCommands(...)`. If the export is missing, commands won't be registered.
321
+
322
+ 4. **Handle interactions at POST /discord** — The platform routes all interactions to this exact path on your worker.
323
+
324
+ 5. **Channels must be in claimed servers** — `sendMessage()`, `editMessage()`, and `addReaction()` only work in channels belonging to servers claimed by your project.
325
+
326
+ 6. **One project per server** — A Discord server can only be claimed by one Fling project at a time.
327
+
328
+ 7. **Plugin must be installed first** — Run `fling plugin install discord` before using any Discord features. Check with `fling plugin permissions discord`.
@@ -333,6 +333,214 @@ Secret names must be uppercase with underscores:
333
333
  - `apiKey` ✗
334
334
  - `github-token` ✗
335
335
 
336
+ ## Storage (R2)
337
+
338
+ Object storage for files and binary data. Uses local filesystem in development, Cloudflare R2 in production.
339
+
340
+ ### Basic Usage
341
+
342
+ ```typescript
343
+ import { storage } from "flingit";
344
+
345
+ // Store a file
346
+ await storage.put("images/logo.png", imageBuffer, { contentType: "image/png" });
347
+
348
+ // Retrieve a file
349
+ const file = await storage.get("images/logo.png");
350
+ if (file) {
351
+ const buffer = await file.arrayBuffer();
352
+ console.log(`Downloaded ${file.size} bytes`);
353
+ }
354
+ ```
355
+
356
+ ### Storing Objects
357
+
358
+ ```typescript
359
+ // String content (auto-detects content type from key extension)
360
+ await storage.put("data/config.json", JSON.stringify({ setting: true }));
361
+
362
+ // Binary data
363
+ const imageBuffer = await fetch("https://example.com/image.png").then(r => r.arrayBuffer());
364
+ await storage.put("images/photo.png", imageBuffer, { contentType: "image/png" });
365
+
366
+ // From request body (streaming)
367
+ app.post("/api/upload", async (c) => {
368
+ const body = await c.req.arrayBuffer();
369
+ const result = await storage.put("uploads/file.bin", body);
370
+ return c.json({ key: result.key, size: result.size });
371
+ });
372
+
373
+ // With custom metadata
374
+ await storage.put("documents/report.pdf", pdfBuffer, {
375
+ contentType: "application/pdf",
376
+ customMetadata: { uploadedBy: "user123", version: "2" }
377
+ });
378
+ ```
379
+
380
+ **Returns:** `StorageObject` with metadata:
381
+ ```typescript
382
+ {
383
+ key: string; // Object key
384
+ size: number; // Size in bytes
385
+ etag: string; // Content hash
386
+ uploaded: Date; // Upload timestamp
387
+ contentType?: string;
388
+ customMetadata?: Record<string, string>;
389
+ }
390
+ ```
391
+
392
+ ### Retrieving Objects
393
+
394
+ ```typescript
395
+ const file = await storage.get("images/logo.png");
396
+
397
+ if (file) {
398
+ // Read as different formats
399
+ const buffer = await file.arrayBuffer(); // Raw bytes
400
+ const text = await file.text(); // UTF-8 string
401
+ const json = await file.json<MyType>(); // Parsed JSON
402
+
403
+ // Or stream the body
404
+ const stream = file.body; // ReadableStream<Uint8Array>
405
+
406
+ // Access metadata
407
+ console.log(file.key); // "images/logo.png"
408
+ console.log(file.size); // 12800
409
+ console.log(file.etag); // '"abc123"'
410
+ console.log(file.uploaded); // Date object
411
+ console.log(file.contentType); // "image/png"
412
+ console.log(file.customMetadata);// { uploadedBy: "user123" }
413
+ }
414
+ ```
415
+
416
+ **Returns:** `StorageObjectBody | null` (null if not found)
417
+
418
+ ### Checking Existence (Head)
419
+
420
+ Get metadata without downloading the content:
421
+
422
+ ```typescript
423
+ const meta = await storage.head("images/logo.png");
424
+
425
+ if (meta) {
426
+ console.log(`File exists: ${meta.size} bytes`);
427
+ console.log(`Uploaded: ${meta.uploaded}`);
428
+ console.log(`Content-Type: ${meta.contentType}`);
429
+ } else {
430
+ console.log("File not found");
431
+ }
432
+ ```
433
+
434
+ **Returns:** `StorageObject | null` (same as put result, null if not found)
435
+
436
+ ### Deleting Objects
437
+
438
+ ```typescript
439
+ // Delete single object
440
+ await storage.delete("old-file.txt");
441
+
442
+ // Delete multiple objects (batch)
443
+ await storage.delete(["file1.txt", "file2.txt", "file3.txt"]);
444
+ ```
445
+
446
+ **Returns:** `void` (no error if object doesn't exist)
447
+
448
+ ### Listing Objects
449
+
450
+ ```typescript
451
+ // List all objects
452
+ const result = await storage.list();
453
+ for (const obj of result.objects) {
454
+ console.log(`${obj.key}: ${obj.size} bytes`);
455
+ }
456
+
457
+ // Filter by prefix
458
+ const images = await storage.list({ prefix: "images/" });
459
+
460
+ // Pagination
461
+ const page1 = await storage.list({ limit: 100 });
462
+ if (page1.truncated) {
463
+ const page2 = await storage.list({ limit: 100, cursor: page1.cursor });
464
+ }
465
+ ```
466
+
467
+ **Options:**
468
+ ```typescript
469
+ {
470
+ prefix?: string; // Filter to keys starting with this
471
+ cursor?: string; // Pagination cursor from previous response
472
+ limit?: number; // Max results (default 1000, max 1000)
473
+ }
474
+ ```
475
+
476
+ **Returns:**
477
+ ```typescript
478
+ {
479
+ objects: StorageObject[]; // Array of object metadata
480
+ truncated: boolean; // true if more results available
481
+ cursor?: string; // Use in next request for pagination
482
+ }
483
+ ```
484
+
485
+ ### Serving Files via HTTP
486
+
487
+ ```typescript
488
+ app.get("/files/:key", async (c) => {
489
+ const key = c.req.param("key");
490
+ const file = await storage.get(key);
491
+
492
+ if (!file) {
493
+ return c.text("Not found", 404);
494
+ }
495
+
496
+ return new Response(file.body, {
497
+ headers: {
498
+ "Content-Type": file.contentType ?? "application/octet-stream",
499
+ "Content-Length": String(file.size),
500
+ "ETag": file.etag,
501
+ }
502
+ });
503
+ });
504
+ ```
505
+
506
+ ### Key Naming Rules
507
+
508
+ - **Max length:** 1024 characters
509
+ - **Cannot start with:** `/`
510
+ - **Cannot contain:** `..`
511
+ - Use path-like structure for organization: `images/2024/photo.png`
512
+
513
+ ### CLI Commands
514
+
515
+ ```bash
516
+ # List objects
517
+ npm exec fling storage list # Local storage
518
+ npm exec fling -- --prod storage list # Production R2
519
+ npm exec fling storage list images/ # Filter by prefix
520
+
521
+ # Upload file
522
+ npm exec fling storage put images/logo.png ./logo.png
523
+ npm exec fling storage put data.json - < data.json # From stdin
524
+
525
+ # Download file
526
+ npm exec fling storage get images/logo.png ./output.png
527
+ npm exec fling storage get data.json # Output to stdout
528
+
529
+ # Delete object
530
+ npm exec fling storage delete old-file.txt --yes
531
+
532
+ # Storage info
533
+ npm exec fling storage info # Local stats
534
+ npm exec fling -- --prod storage info # Production R2 stats
535
+ ```
536
+
537
+ ### Important Notes
538
+
539
+ - **Storage is provisioned automatically** on first `fling push` - no setup required
540
+ - **Keys are flat** - `images/logo.png` is just a string, not a directory structure
541
+ - **100MB max object size** for direct uploads
542
+ - **Streaming** - Large files are streamed, not buffered in memory
543
+
336
544
  ## WebAssembly (WASM)
337
545
 
338
546
  Fling supports importing WebAssembly modules in your backend code. This enables using libraries like `@resvg/resvg-wasm` for SVG rendering, image processing, and other compute-intensive tasks.