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.
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +29 -16
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/cli/commands/cron.d.ts.map +1 -1
- package/dist/cli/commands/cron.js +7 -3
- package/dist/cli/commands/cron.js.map +1 -1
- package/dist/cli/commands/db.d.ts.map +1 -1
- package/dist/cli/commands/db.js +5 -2
- package/dist/cli/commands/db.js.map +1 -1
- package/dist/cli/commands/feedback.d.ts.map +1 -1
- package/dist/cli/commands/feedback.js +6 -1
- package/dist/cli/commands/feedback.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +5 -20
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/launch.d.ts.map +1 -1
- package/dist/cli/commands/launch.js +5 -59
- package/dist/cli/commands/launch.js.map +1 -1
- package/dist/cli/commands/login.d.ts.map +1 -1
- package/dist/cli/commands/login.js +7 -2
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/logs.d.ts.map +1 -1
- package/dist/cli/commands/logs.js +3 -1
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/onboard.js +1 -1
- package/dist/cli/commands/plugin.d.ts +8 -0
- package/dist/cli/commands/plugin.d.ts.map +1 -0
- package/dist/cli/commands/plugin.js +407 -0
- package/dist/cli/commands/plugin.js.map +1 -0
- package/dist/cli/commands/project.d.ts.map +1 -1
- package/dist/cli/commands/project.js +3 -1
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/push.d.ts.map +1 -1
- package/dist/cli/commands/push.js +128 -12
- package/dist/cli/commands/push.js.map +1 -1
- package/dist/cli/commands/register.d.ts +25 -0
- package/dist/cli/commands/register.d.ts.map +1 -1
- package/dist/cli/commands/register.js +30 -13
- package/dist/cli/commands/register.js.map +1 -1
- package/dist/cli/commands/secret.d.ts +9 -0
- package/dist/cli/commands/secret.d.ts.map +1 -1
- package/dist/cli/commands/secret.js +40 -50
- package/dist/cli/commands/secret.js.map +1 -1
- package/dist/cli/commands/storage.d.ts +10 -0
- package/dist/cli/commands/storage.d.ts.map +1 -0
- package/dist/cli/commands/storage.js +459 -0
- package/dist/cli/commands/storage.js.map +1 -0
- package/dist/cli/commands/tun.d.ts.map +1 -1
- package/dist/cli/commands/tun.js +3 -2
- package/dist/cli/commands/tun.js.map +1 -1
- package/dist/cli/deploy/bundler.d.ts +15 -0
- package/dist/cli/deploy/bundler.d.ts.map +1 -1
- package/dist/cli/deploy/bundler.js +86 -2
- package/dist/cli/deploy/bundler.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/config.d.ts +12 -0
- package/dist/cli/utils/config.d.ts.map +1 -1
- package/dist/cli/utils/config.js +23 -3
- package/dist/cli/utils/config.js.map +1 -1
- package/dist/cli/utils/environment.d.ts +12 -0
- package/dist/cli/utils/environment.d.ts.map +1 -1
- package/dist/cli/utils/environment.js +40 -6
- package/dist/cli/utils/environment.js.map +1 -1
- package/dist/cli/utils/launch-io-impl.d.ts +13 -0
- package/dist/cli/utils/launch-io-impl.d.ts.map +1 -0
- package/dist/cli/utils/launch-io-impl.js +51 -0
- package/dist/cli/utils/launch-io-impl.js.map +1 -0
- package/dist/cli/utils/launch-io.d.ts +42 -0
- package/dist/cli/utils/launch-io.d.ts.map +1 -0
- package/dist/cli/utils/launch-io.js +8 -0
- package/dist/cli/utils/launch-io.js.map +1 -0
- package/dist/cli/utils/project-name.d.ts +17 -0
- package/dist/cli/utils/project-name.d.ts.map +1 -0
- package/dist/cli/utils/project-name.js +33 -0
- package/dist/cli/utils/project-name.js.map +1 -0
- package/dist/cli/utils/prompt-new-project.d.ts +12 -0
- package/dist/cli/utils/prompt-new-project.d.ts.map +1 -0
- package/dist/cli/utils/prompt-new-project.js +66 -0
- package/dist/cli/utils/prompt-new-project.js.map +1 -0
- package/dist/cli/utils/version-check.d.ts +30 -0
- package/dist/cli/utils/version-check.d.ts.map +1 -0
- package/dist/cli/utils/version-check.js +43 -0
- package/dist/cli/utils/version-check.js.map +1 -0
- package/dist/client/assets/index-BqVrS7t9.js +40 -0
- package/dist/client/assets/index-DSLUsCtO.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/client/vite.svg +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/db.d.ts +5 -0
- package/dist/runtime/db.d.ts.map +1 -1
- package/dist/runtime/db.js +6 -1
- package/dist/runtime/db.js.map +1 -1
- package/dist/runtime/discord.d.ts +171 -0
- package/dist/runtime/discord.d.ts.map +1 -0
- package/dist/runtime/discord.js +192 -0
- package/dist/runtime/discord.js.map +1 -0
- package/dist/runtime/secrets.d.ts +28 -0
- package/dist/runtime/secrets.d.ts.map +1 -1
- package/dist/runtime/secrets.js +77 -20
- package/dist/runtime/secrets.js.map +1 -1
- package/dist/runtime/storage.d.ts +29 -0
- package/dist/runtime/storage.d.ts.map +1 -0
- package/dist/runtime/storage.js +456 -0
- package/dist/runtime/storage.js.map +1 -0
- package/dist/shared/constants.d.ts +12 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +12 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/discord-types.d.ts +130 -0
- package/dist/shared/discord-types.d.ts.map +1 -0
- package/dist/shared/discord-types.js +8 -0
- package/dist/shared/discord-types.js.map +1 -0
- package/dist/shared/random-code.d.ts +6 -0
- package/dist/shared/random-code.d.ts.map +1 -0
- package/dist/shared/random-code.js +23 -0
- package/dist/shared/random-code.js.map +1 -0
- package/dist/types/storage.d.ts +154 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +8 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/worker-runtime/discord.d.ts +61 -0
- package/dist/worker-runtime/discord.d.ts.map +1 -0
- package/dist/worker-runtime/discord.js +181 -0
- package/dist/worker-runtime/discord.js.map +1 -0
- package/dist/worker-runtime/index.d.ts +21 -1
- package/dist/worker-runtime/index.d.ts.map +1 -1
- package/dist/worker-runtime/index.js +102 -0
- package/dist/worker-runtime/index.js.map +1 -1
- package/package.json +9 -2
- package/templates/default/CLAUDE.md +65 -11
- package/templates/default/skills/discord/SKILL.md +328 -0
- package/templates/default/skills/fling/API.md +208 -0
- package/templates/default/skills/fling/SKILL.md +51 -8
- package/dist/cli/deploy/worker-entry.d.ts +0 -14
- package/dist/cli/deploy/worker-entry.d.ts.map +0 -1
- package/dist/cli/deploy/worker-entry.js +0 -60
- package/dist/cli/deploy/worker-entry.js.map +0 -1
- package/dist/cli/deploy/wrangler-config.d.ts +0 -20
- package/dist/cli/deploy/wrangler-config.d.ts.map +0 -1
- package/dist/cli/deploy/wrangler-config.js +0 -54
- package/dist/cli/deploy/wrangler-config.js.map +0 -1
- package/dist/cli/loaders/register-wasm.d.ts +0 -12
- package/dist/cli/loaders/register-wasm.d.ts.map +0 -1
- package/dist/cli/loaders/register-wasm.js +0 -19
- package/dist/cli/loaders/register-wasm.js.map +0 -1
- package/dist/cli/loaders/wasm-hooks.d.ts +0 -61
- package/dist/cli/loaders/wasm-hooks.d.ts.map +0 -1
- package/dist/cli/loaders/wasm-hooks.js +0 -63
- 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
|
|
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:**
|
|
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
|
|
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
|
-
//
|
|
223
|
-
|
|
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
|
|
234
|
-
-
|
|
235
|
-
-
|
|
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.
|