@jchat/create-addon 1.0.0-alpha.3 → 1.0.0-alpha.30

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.
@@ -1,83 +1,260 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- //#region ../addon-sdk/src/diagnostics.ts
4
- function createCompatibleMajorRange(version) {
5
- try {
6
- Bun.semver.order(version, version);
7
- } catch {
8
- throw new Error(`Invalid semantic version: ${version}`);
9
- }
10
- const majorComponent = /^(?:v|=)?(\d+)\./.exec(version)?.[1];
11
- const major = majorComponent ? Number(majorComponent) : NaN;
12
- if (!Number.isSafeInteger(major)) throw new Error(`Invalid semantic version: ${version}`);
13
- return `>=${version} <${major + 1}.0.0`;
1
+ import{n as e,t}from"./contracts2.d.mts";import*as n from"node:fs";import*as r from"node:path";var[i]=[8,()=>[],[``]];function a(e){return`import type { ColumnType, Selectable } from "@jchat/addon-sdk"
2
+
3
+ export interface AddonItemTable {
4
+ id: string
5
+ siteId: string
6
+ roomId: string
7
+ userId: string
8
+ title: string
9
+ createdAt: ColumnType<Date, Date | string, Date | string>
10
+ }
11
+
12
+ export interface AddonDatabase {
13
+ ${`${e.replace(/-/g,`_`)}_items`}: AddonItemTable
14
14
  }
15
- //#endregion
16
- //#region src/generator.ts
17
- const ADDON_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
18
- const DEFAULT_SANDBOX_IMAGE = "ghcr.io/mjjabarullah/addon-developer-sandbox";
19
- function createStandaloneDevkitFiles(options) {
20
- if (!ADDON_ID_PATTERN.test(options.addonId)) throw new Error("Addon id must contain lowercase letters, numbers, and hyphens");
21
- const image = options.sandboxImage ?? `${DEFAULT_SANDBOX_IMAGE}:${options.jchatVersion.replace(/\+/g, "-")}`;
22
- const manifest = {
23
- id: options.addonId,
24
- name: toDisplayName(options.addonId),
25
- version: "1.0.0",
26
- description: `${toDisplayName(options.addonId)} addon for JChat`,
27
- author: "Developer",
28
- compatibility: {
29
- jchat: createCompatibleMajorRange(options.jchatVersion),
30
- addonSdk: createCompatibleMajorRange(options.addonSdkVersion)
31
- },
32
- frontend: {
33
- bundle: "./ui.js",
34
- mountPoints: ["chat-toolbar"]
35
- },
36
- permissions: []
37
- };
38
- return Object.freeze({
39
- ".env.example": createEnvironmentFile(options.addonId, image),
40
- ".gitignore": [
41
- "node_modules/",
42
- "addon/node_modules/",
43
- "addon/build/",
44
- ".env",
45
- ""
46
- ].join("\n"),
47
- "README.md": createReadme(options.addonId),
48
- "docker-compose.yml": createComposeFile(),
49
- "package.json": `${JSON.stringify(createRootPackage(options.addonId), null, 2)}\n`,
50
- "scripts/devkit.ts": createLifecycleScript(),
51
- "addon/package.json": `${JSON.stringify(createAddonPackage(options.addonId, options.addonSdkVersion), null, 2)}\n`,
52
- "addon/tsconfig.json": `${JSON.stringify(createTsConfig(), null, 2)}\n`,
53
- "addon/vite.config.ts": createViteConfig(),
54
- "addon/src/manifest.ts": createManifestSource(manifest),
55
- "addon/src/tailwind.css": createTailwindSource(),
56
- "addon/src/ui.tsx": createUiSource(manifest.name)
57
- });
15
+
16
+ export type AddonItem = Selectable<AddonItemTable>
17
+ `}function o(e){let t=`${e.replace(/-/g,`_`)}_items`;return`CREATE TABLE IF NOT EXISTS "addons"."${t}" (
18
+ "id" VARCHAR(36) PRIMARY KEY,
19
+ "site_id" VARCHAR(36) NOT NULL,
20
+ "room_id" VARCHAR(36) NOT NULL,
21
+ "user_id" VARCHAR(36) NOT NULL,
22
+ "title" VARCHAR(255) NOT NULL,
23
+ "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
24
+ );
25
+
26
+ CREATE INDEX IF NOT EXISTS "${`idx_${t}_site_room`}"
27
+ ON "addons"."${t}" ("site_id", "room_id", "created_at" DESC);
28
+ `}function s(e){let t=`${e.replace(/-/g,`_`)}_items`;return`import type { AddonApp, AddonServerContext } from "@jchat/addon-sdk"
29
+ import type { AddonDatabase } from "./db"
30
+
31
+ export default (
32
+ {
33
+ db,
34
+ t,
35
+ cache,
36
+ emit,
37
+ getUser,
38
+ hasPermission
39
+ }: AddonServerContext<AddonDatabase>,
40
+ app: AddonApp
41
+ ) => {
42
+ return app
43
+ .get(
44
+ "/items",
45
+ async ({ query, user }) => {
46
+ const siteId = user?.siteId ?? ""
47
+ const roomId = query.roomId ?? ""
48
+ const items = await db
49
+ .selectFrom("${t}")
50
+ .where("siteId", "=", siteId)
51
+ .where("roomId", "=", roomId)
52
+ .orderBy("createdAt", "desc")
53
+ .limit(20)
54
+ .selectAll()
55
+ .execute()
56
+
57
+ return { items }
58
+ },
59
+ {
60
+ query: t.Object({
61
+ roomId: t.Optional(t.String())
62
+ })
63
+ }
64
+ )
65
+ .post(
66
+ "/items",
67
+ async ({ body, user, set }) => {
68
+ if (!user) {
69
+ set.status = 401
70
+ return { error: "Unauthorized" }
71
+ }
72
+
73
+ const id = Bun.randomUUIDv7()
74
+ const siteId = user.siteId
75
+ const roomId = body.roomId
76
+ const title = body.title
77
+
78
+ await db
79
+ .insertInto("${t}")
80
+ .values({
81
+ id,
82
+ siteId,
83
+ roomId,
84
+ userId: user.id,
85
+ title,
86
+ createdAt: new Date()
87
+ })
88
+ .execute()
89
+
90
+ await emit("addon:${e}:item-created", { id, title, userId: user.id })
91
+ return { id, title }
92
+ },
93
+ {
94
+ body: t.Object({
95
+ roomId: t.String(),
96
+ title: t.String({ minLength: 1, maxLength: 255 })
97
+ })
98
+ }
99
+ )
58
100
  }
59
- function writeStandaloneDevkit(options) {
60
- const targetDir = path.resolve(options.targetDir);
61
- if (fs.existsSync(targetDir)) throw new Error(`Development kit target already exists: ${targetDir}`);
62
- for (const [relativePath, contents] of Object.entries(createStandaloneDevkitFiles(options))) {
63
- const target = path.join(targetDir, ...relativePath.split("/"));
64
- fs.mkdirSync(path.dirname(target), { recursive: true });
65
- fs.writeFileSync(target, contents);
66
- }
101
+ `}function c(e,t,n){return n?`import type { AddonProps } from "@jchat/addon-sdk"
102
+ import { useAddonMutation, useAddonQuery, useAddonSDK, useTranslation } from "@jchat/addon-sdk"
103
+ import Icons from "@jchat/addon-sdk/icon"
104
+ import { useState } from "react"
105
+ import locales from "./locales"
106
+ import "./tailwind.css"
107
+
108
+ interface ItemRecord {
109
+ id: string
110
+ title: string
111
+ createdAt: string
67
112
  }
68
- function createEnvironmentFile(addonId, image) {
69
- return [
70
- `JCHAT_SANDBOX_IMAGE=${image}`,
71
- `JCHAT_SANDBOX_ADDON_ID=${addonId}`,
72
- "JCHAT_SANDBOX_PORT=4317",
73
- "JCHAT_SANDBOX_HTTP_MOCK_PORT=4318",
74
- "JCHAT_SANDBOX_HTTP_MOCK_OFFLINE=false",
75
- "JCHAT_SANDBOX_ADDON_HTTP_MOCK_URL=http://http-mock:4318",
76
- ""
77
- ].join("\n");
113
+
114
+ export default function Addon({ mountPoint, roomId }: AddonProps) {
115
+ const sdk = useAddonSDK()
116
+ const { t, dir } = useTranslation(locales)
117
+ const [title, setTitle] = useState("")
118
+
119
+ const { data, refetch, isLoading } = useAddonQuery<{ items: ItemRecord[] }>({
120
+ path: \`/items?roomId=\${roomId ?? ""}\`
121
+ })
122
+
123
+ const { mutate: createItem, isPending } = useAddonMutation<
124
+ { id: string; title: string },
125
+ { roomId: string; title: string }
126
+ >({
127
+ path: "/items",
128
+ onSuccess: () => {
129
+ setTitle("")
130
+ refetch()
131
+ sdk.toast(t("toast_message", { name: "${e}" }))
132
+ }
133
+ })
134
+
135
+ if (mountPoint === "chat-sidebar") {
136
+ return (
137
+ <aside dir={dir} className="flex h-full flex-col border-s border-border bg-card p-4 text-card-foreground">
138
+ <header className="mb-4 flex items-center gap-2 border-b border-border pb-2">
139
+ <Icons.Plus aria-hidden="true" className="size-5 text-primary" />
140
+ <h2 className="font-semibold">${e}</h2>
141
+ </header>
142
+ <p className="mb-3 text-xs text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
143
+ <form
144
+ className="mb-4 flex gap-2"
145
+ onSubmit={(e) => {
146
+ e.preventDefault()
147
+ if (!title.trim() || !roomId) return
148
+ createItem({ roomId, title: title.trim() })
149
+ }}
150
+ >
151
+ <input
152
+ type="text"
153
+ value={title}
154
+ onChange={(e) => setTitle(e.target.value)}
155
+ placeholder="Add new item..."
156
+ className="flex-1 rounded border border-input bg-background px-2 py-1 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
157
+ />
158
+ <button
159
+ type="submit"
160
+ disabled={isPending || !title.trim()}
161
+ className="rounded bg-primary px-3 py-1 text-sm text-primary-foreground disabled:opacity-50"
162
+ >
163
+ {isPending ? "..." : t("test_button")}
164
+ </button>
165
+ </form>
166
+ <div className="flex-1 space-y-1 overflow-y-auto">
167
+ {isLoading && <p className="text-xs text-muted-foreground">Loading...</p>}
168
+ {data?.items?.map((item) => (
169
+ <div key={item.id} className="rounded border border-border/50 bg-background/50 p-2 text-xs">
170
+ {item.title}
171
+ </div>
172
+ ))}
173
+ </div>
174
+ </aside>
175
+ )
176
+ }
177
+
178
+ return (
179
+ <div dir={dir} className="rounded border border-border bg-card p-2 text-card-foreground">
180
+ <div className="mb-2 flex items-center justify-between gap-2">
181
+ <div className="flex items-center gap-2">
182
+ <Icons.Plus aria-hidden="true" className="size-4 text-primary" />
183
+ <strong>${e}</strong>
184
+ </div>
185
+ <span className="text-xs text-muted-foreground">{t("mounted_at", { mountPoint })}</span>
186
+ </div>
187
+ <div className="flex gap-2">
188
+ <input
189
+ type="text"
190
+ value={title}
191
+ onChange={(e) => setTitle(e.target.value)}
192
+ placeholder="New item..."
193
+ className="flex-1 rounded border border-input bg-background px-2 py-1 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
194
+ />
195
+ <button
196
+ type="button"
197
+ disabled={isPending || !title.trim()}
198
+ className="rounded bg-primary px-3 py-1 text-xs text-primary-foreground disabled:opacity-50"
199
+ onClick={() => {
200
+ if (!title.trim()) return
201
+ createItem({ roomId: roomId ?? "general", title: title.trim() })
202
+ }}
203
+ >
204
+ {isPending ? "..." : t("test_button")}
205
+ </button>
206
+ </div>
207
+ </div>
208
+ )
78
209
  }
79
- function createComposeFile() {
80
- return `services:
210
+ `:`import type { AddonProps } from "@jchat/addon-sdk"
211
+ import { useAddonSDK, useTranslation } from "@jchat/addon-sdk"
212
+ import Icons from "@jchat/addon-sdk/icon"
213
+ import locales from "./locales"
214
+ import "./tailwind.css"
215
+
216
+ export default function Addon({ mountPoint }: AddonProps) {
217
+ const sdk = useAddonSDK()
218
+ const { t, dir } = useTranslation(locales)
219
+
220
+ if (mountPoint === "chat-sidebar") {
221
+ return (
222
+ <aside dir={dir} className="flex h-full flex-col border-s border-border bg-card p-4 text-card-foreground">
223
+ <header className="mb-4 flex items-center gap-2 border-b border-border pb-2">
224
+ <Icons.Plus aria-hidden="true" className="size-5 text-primary" />
225
+ <h2 className="font-semibold">${e}</h2>
226
+ </header>
227
+ <p className="mb-2 text-sm text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
228
+ <button
229
+ type="button"
230
+ className="mt-4 rounded bg-primary px-3 py-2 text-primary-foreground"
231
+ onClick={() => sdk.toast(t("toast_message", { name: "${e}" }))}
232
+ >
233
+ {t("test_button")}
234
+ </button>
235
+ </aside>
236
+ )
237
+ }
238
+
239
+ return (
240
+ <div dir={dir} className="rounded border border-border bg-card p-2 text-card-foreground">
241
+ <div className="mb-2 flex items-center gap-2">
242
+ <Icons.Plus aria-hidden="true" className="size-4 text-primary" />
243
+ <strong>${e}</strong>
244
+ </div>
245
+ <p className="mb-2 text-sm text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
246
+ <button
247
+ type="button"
248
+ className="rounded bg-primary px-3 py-2 text-primary-foreground"
249
+ onClick={() => sdk.toast(t("toast_message", { name: "${e}" }))}
250
+ >
251
+ {t("test_button")}
252
+ </button>
253
+ </div>
254
+ )
255
+ }
256
+ `}function l(e,t){return[`JCHAT_SANDBOX_IMAGE=${t}`,`JCHAT_SANDBOX_ADDON_ID=${e}`,`JCHAT_SANDBOX_PORT=4317`,`JCHAT_SANDBOX_HTTP_MOCK_PORT=4318`,`JCHAT_SANDBOX_HTTP_MOCK_OFFLINE=false`,`JCHAT_SANDBOX_ADDON_HTTP_MOCK_URL=http://http-mock:4318`,``].join(`
257
+ `)}function u(){return`services:
81
258
  sandbox:
82
259
  image: \${JCHAT_SANDBOX_IMAGE:?Copy .env.example to .env}
83
260
  restart: unless-stopped
@@ -87,9 +264,6 @@ function createComposeFile() {
87
264
  JCHAT_COOKIE_SECRET: addon-sandbox-cookie-secret
88
265
  JCHAT_DATABASE_URL: postgresql://jchat:jchat@db:5432/jchat
89
266
  JCHAT_REDIS_DATABASE_URL: redis://redis:6379
90
- JCHAT_STORAGE_TYPE: LOCAL
91
- JCHAT_BOT_NAME: Sandbox Bot
92
- JCHAT_BOT_EMAIL: bot@sandbox.local
93
267
  JCHAT_DOMAIN: localhost
94
268
  JCHAT_SITE_URL: http://localhost:\${JCHAT_SANDBOX_PORT:-4317}
95
269
  JCHAT_ROUTING_MODE: path
@@ -98,7 +272,7 @@ function createComposeFile() {
98
272
  ports:
99
273
  - "127.0.0.1:\${JCHAT_SANDBOX_PORT:-4317}:4317"
100
274
  volumes:
101
- - ./addon:/app/addons-dev/\${JCHAT_SANDBOX_ADDON_ID:?Set addon id}:ro
275
+ - ./addon:/app/addons/\${JCHAT_SANDBOX_ADDON_ID:?Set addon id}:ro
102
276
  - sandbox_uploads:/app/uploads
103
277
  depends_on:
104
278
  db:
@@ -150,159 +324,7 @@ volumes:
150
324
  sandbox_postgres:
151
325
  sandbox_redis:
152
326
  sandbox_uploads:
153
- `;
154
- }
155
- function createRootPackage(addonId) {
156
- return {
157
- name: `${addonId}-development-kit`,
158
- private: true,
159
- type: "module",
160
- workspaces: ["addon"],
161
- scripts: {
162
- sandbox: "bun run ./scripts/devkit.ts run",
163
- "sandbox:test": "bun run ./scripts/devkit.ts test",
164
- "sandbox:reset": "bun run ./scripts/devkit.ts reset",
165
- "sandbox:package": "bun run ./scripts/devkit.ts package",
166
- "sandbox:cleanup": "bun run ./scripts/devkit.ts cleanup"
167
- },
168
- devDependencies: { "bun-types": "^1.3.14" },
169
- engines: { bun: ">=1.3.11" }
170
- };
171
- }
172
- function createAddonPackage(addonId, addonSdkVersion) {
173
- return {
174
- name: `@jchat/${addonId}`,
175
- version: "1.0.0",
176
- private: true,
177
- type: "module",
178
- scripts: {
179
- dev: "vite build --watch --mode=development",
180
- build: "vite build --mode=production",
181
- test: "bun test",
182
- typecheck: "tsc --noEmit"
183
- },
184
- dependencies: {
185
- "@jchat/addon-sdk": `^${addonSdkVersion}`,
186
- "@tanstack/react-query": "^5.101.4",
187
- react: "^19.2.8",
188
- "react-dom": "^19.2.8"
189
- },
190
- devDependencies: {
191
- "@tailwindcss/vite": "^4.3.3",
192
- "@types/node": "^26.1.1",
193
- "@types/react": "^19.2.17",
194
- "@types/react-dom": "^19.2.3",
195
- "bun-types": "^1.3.14",
196
- tailwindcss: "^4.3.3",
197
- typescript: "^7.0.2",
198
- vite: "^8.1.5"
199
- }
200
- };
201
- }
202
- function createTsConfig() {
203
- return {
204
- compilerOptions: {
205
- strict: true,
206
- target: "ESNext",
207
- module: "ESNext",
208
- moduleResolution: "bundler",
209
- jsx: "react-jsx",
210
- noEmit: true,
211
- types: ["bun-types", "node"]
212
- },
213
- include: [
214
- "src/**/*.ts",
215
- "src/**/*.tsx",
216
- "vite.config.ts"
217
- ]
218
- };
219
- }
220
- function createManifestSource(manifest) {
221
- return `import type { AddonManifest } from "@jchat/addon-sdk"
222
-
223
- const manifest: AddonManifest = ${JSON.stringify(manifest, null, 2)}
224
-
225
- export default manifest
226
- `;
227
- }
228
- function createViteConfig() {
229
- return `import { jchatAddon } from "@jchat/addon-sdk/vite"
230
- import tailwindcss from "@tailwindcss/vite"
231
- import path from "node:path"
232
- import { defineConfig } from "vite"
233
- import manifest from "./src/manifest"
234
-
235
- export default defineConfig({
236
- plugins: [tailwindcss(), jchatAddon({ manifest })],
237
- build: {
238
- outDir: "build",
239
- emptyOutDir: true,
240
- lib: {
241
- entry: path.resolve(import.meta.dirname, "src/ui.tsx"),
242
- formats: ["es"],
243
- fileName: () => "ui.[hash].js"
244
- },
245
- rolldownOptions: {
246
- external: [
247
- "react",
248
- "react-dom",
249
- "react/jsx-runtime",
250
- "@tanstack/react-query",
251
- "@jchat/addon-sdk"
252
- ],
253
- output: {
254
- format: "esm"
255
- }
256
- }
257
- }
258
- })
259
- `;
260
- }
261
- function createUiSource(name) {
262
- return `import type { AddonProps } from "@jchat/addon-sdk"
263
- import { useAddonSDK, useIcons } from "@jchat/addon-sdk"
264
- import "./tailwind.css"
265
-
266
- export default function Addon({ mountPoint }: AddonProps) {
267
- const sdk = useAddonSDK()
268
- const { Icons } = useIcons()
269
-
270
- return (
271
- <div className="rounded border border-border bg-card p-2 text-card-foreground">
272
- <div className="mb-2 flex items-center gap-2">
273
- <Icons.Plus aria-hidden="true" className="size-4 text-primary" />
274
- <strong>${name}</strong>
275
- </div>
276
- <p className="mb-2 text-sm text-muted-foreground">Mounted at {mountPoint}</p>
277
- <button
278
- type="button"
279
- className="rounded bg-primary px-3 py-2 text-primary-foreground"
280
- onClick={() => sdk.toast("Hello from ${name}")}
281
- >
282
- Test addon
283
- </button>
284
- </div>
285
- )
286
- }
287
- `;
288
- }
289
- function createTailwindSource() {
290
- return `@import "tailwindcss";
291
-
292
- @theme inline {
293
- --color-background: var(--background);
294
- --color-foreground: var(--foreground);
295
- --color-card: var(--card);
296
- --color-card-foreground: var(--card-foreground);
297
- --color-border: var(--border);
298
- --color-primary: var(--primary);
299
- --color-primary-foreground: var(--primary-foreground);
300
- --color-muted-foreground: var(--muted-foreground);
301
- }
302
- `;
303
- }
304
- function createLifecycleScript() {
305
- return `import * as fs from "node:fs"
327
+ `}function d(){return`import * as fs from "node:fs"
306
328
  import * as path from "node:path"
307
329
 
308
330
  type Command = "run" | "test" | "reset" | "package" | "cleanup"
@@ -314,7 +336,7 @@ if (!["run", "test", "reset", "package", "cleanup"].includes(command)) {
314
336
 
315
337
  const root = path.resolve(import.meta.dir, "..")
316
338
  const environment = {
317
- ...process.env,
339
+ ...Bun.env,
318
340
  ...readEnvironment(path.join(root, ".env"))
319
341
  }
320
342
  const addonId = environment.JCHAT_SANDBOX_ADDON_ID
@@ -371,12 +393,9 @@ function readEnvironment(file: string): Record<string, string> {
371
393
  })
372
394
  )
373
395
  }
374
- `;
375
- }
376
- function createReadme(addonId) {
377
- return `# ${toDisplayName(addonId)} Development Kit
396
+ `}function f(e,t){return`# ${t} Development Kit
378
397
 
379
- This workspace develops \`${addonId}\` against the license-free JChat developer sandbox without
398
+ This workspace develops \`${e}\` against the license-free JChat developer sandbox without
380
399
  including JChat Core source.
381
400
 
382
401
  ## Start
@@ -400,10 +419,285 @@ Open \`http://127.0.0.1:4317\`. The HTTP fixture recorder is available at
400
419
 
401
420
  The referenced JChat packages and sandbox image must be published before this standalone kit can
402
421
  install or start outside the JChat repository.
403
- `;
422
+ `}const p={ar:e=>({welcome:`مرحبًا بك في ${e}`,mounted_at:`تم التثبيت في {{mountPoint}}`,test_button:`اختبار الإضافة`,toast_message:`مرحبًا من ${e}`}),bg:e=>({welcome:`Добре дошли в ${e}`,mounted_at:`Монтирано на {{mountPoint}}`,test_button:`Тестване на добавката`,toast_message:`Здравейте от ${e}`}),de:e=>({welcome:`Willkommen bei ${e}`,mounted_at:`Eingebunden an {{mountPoint}}`,test_button:`Addon testen`,toast_message:`Hallo von ${e}`}),el:e=>({welcome:`Καλώς ήρθατε στο ${e}`,mounted_at:`Προσαρτήθηκε στο {{mountPoint}}`,test_button:`Δοκιμή πρόσθετου`,toast_message:`Γεια σας από το ${e}`}),en:e=>({welcome:`Welcome to ${e}`,mounted_at:`Mounted at {{mountPoint}}`,test_button:`Test addon`,toast_message:`Hello from ${e}`}),es:e=>({welcome:`Bienvenido a ${e}`,mounted_at:`Montado en {{mountPoint}}`,test_button:`Probar complemento`,toast_message:`Hola desde ${e}`}),fr:e=>({welcome:`Bienvenue sur ${e}`,mounted_at:`Monté sur {{mountPoint}}`,test_button:`Tester le module`,toast_message:`Bonjour de ${e}`}),he:e=>({welcome:`ברוכים הבאים אל ${e}`,mounted_at:`מותקן ב-{{mountPoint}}`,test_button:`בדוק תוסף`,toast_message:`שלום מ-${e}`}),hi:e=>({welcome:`${e} में आपका स्वागत है`,mounted_at:`{{mountPoint}} पर स्थापित`,test_button:`ऐडऑन का परीक्षण करें`,toast_message:`${e} से नमस्ते`}),hr:e=>({welcome:`Dobrodošli u ${e}`,mounted_at:`Postavljeno na {{mountPoint}}`,test_button:`Testiraj dodatak`,toast_message:`Pozdrav iz ${e}`}),kn:e=>({welcome:`${e} ಗೆ ಸುಸ್ವಾಗತ`,mounted_at:`{{mountPoint}} ನಲ್ಲಿ ಸ್ಥಾಪಿಸಲಾಗಿದೆ`,test_button:`ಆಡ್ಆನ್ ಪರೀಕ್ಷಿಸಿ`,toast_message:`${e} ಇಂದ ನಮಸ್ಕಾರ`}),ml:e=>({welcome:`${e}-ലേക്ക് സ്വാഗതം`,mounted_at:`{{mountPoint}}-ൽ ഘടിപ്പിച്ചു`,test_button:`ആഡ്ഓൺ പരിശോധിക്കുക`,toast_message:`${e}-ൽ നിന്നുള്ള ആശംസകൾ`}),nl:e=>({welcome:`Welkom bij ${e}`,mounted_at:`Gekoppeld op {{mountPoint}}`,test_button:`Addon testen`,toast_message:`Hallo vanuit ${e}`}),pt:e=>({welcome:`Bem-vindo ao ${e}`,mounted_at:`Montado em {{mountPoint}}`,test_button:`Testar extensão`,toast_message:`Olá de ${e}`}),ro:e=>({welcome:`Bun venit la ${e}`,mounted_at:`Montat la {{mountPoint}}`,test_button:`Testează extensia`,toast_message:`Bună din ${e}`}),ru:e=>({welcome:`Добро пожаловать в ${e}`,mounted_at:`Подключено в {{mountPoint}}`,test_button:`Проверить дополнение`,toast_message:`Привет от ${e}`}),ta:e=>({welcome:`${e}-க்கு வரவேற்கிறோம்`,mounted_at:`{{mountPoint}}-ல் பொருத்தப்பட்டுள்ளது`,test_button:`செருகுநிரலை சோதிக்கவும்`,toast_message:`${e}-லிருந்து வணக்கம்`}),te:e=>({welcome:`${e}కి స్వాగతం`,mounted_at:`{{mountPoint}} వద్ద మౌంట్ చేయబడింది`,test_button:`యాడ్ఆన్‌ని పరీక్షించండి`,toast_message:`${e} నుండి నమస్కారం`}),tr:e=>({welcome:`${e}'e hoş geldiniz`,mounted_at:`{{mountPoint}} konumuna yerleştirildi`,test_button:`Eklentiyi test et`,toast_message:`${e}'den merhaba`}),ur:e=>({welcome:`${e} میں خوش آمدید`,mounted_at:`{{mountPoint}} پر نصب ہے`,test_button:`ایڈ آن کا تجربہ کریں`,toast_message:`${e} کی طرف سے ہیلو`})};function m(){return`${t.map(e=>`import ${e} from "./${e}.json"`).join(`
423
+ `)}
424
+
425
+ export type TranslationKey = keyof typeof en
426
+
427
+ export const locales = {
428
+ ${t.map(e=>` ${e}`).join(`,
429
+ `)}
430
+ } as const
431
+
432
+ export default locales
433
+ `}function h(){return`import { describe, expect, test } from "bun:test"
434
+ import { validateAddonLocales } from "@jchat/addon-sdk/i18n-validator"
435
+ import * as path from "node:path"
436
+
437
+ describe("addon locales", () => {
438
+ test("maintains key parity across all 20 host languages", () => {
439
+ const localesDir = path.join(import.meta.dir, "locales")
440
+ const result = validateAddonLocales(localesDir)
441
+ if (!result.valid) {
442
+ console.error(result.errors.join("\\n"))
443
+ }
444
+ expect(result.valid).toBe(true)
445
+ })
446
+ })
447
+ `}function g(e){let n={"src/locales/index.ts":m(),"src/locales.test.ts":h()};for(let r of t){let t=p[r];n[`src/locales/${r}.json`]=`${JSON.stringify(t(e),null,2)}\n`}return n}function _(e){return`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
448
+ <defs>
449
+ <linearGradient id="addon-grad" x1="0%" y1="0%" x2="100%" y2="100%">
450
+ <stop offset="0%" stop-color="#6366f1" />
451
+ <stop offset="100%" stop-color="#a855f7" />
452
+ </linearGradient>
453
+ </defs>
454
+ <rect width="128" height="128" rx="28" fill="url(#addon-grad)" />
455
+ <circle cx="64" cy="64" r="42" fill="white" fill-opacity="0.15" />
456
+ <text x="64" y="76" font-family="system-ui, -apple-system, sans-serif" font-size="44" font-weight="800" fill="white" text-anchor="middle" dominant-baseline="middle">${e.charAt(0).toUpperCase()||`A`}</text>
457
+ </svg>
458
+ `}function v(e){return`import type { AddonManifest } from "@jchat/addon-sdk"
459
+
460
+ const manifest: AddonManifest = ${JSON.stringify(e,null,2)}
461
+
462
+ export default manifest
463
+ `}function y(){return`@import "@jchat/addon-sdk/ui/tailwind.css";`}var b={name:`jchat`,version:`0.0.0`,private:!0,author:{name:`Jchat Cloud`,email:`support@jchatcloud.com`,url:`https://github.com/jchatcloud`},workspaces:[`apps/*`,`!apps/android`,`packages/*`,`addons/*`],scripts:{dev:`bun run --parallel chat:dev addons:dev`,"chat:dev":`bun --watch apps/chat/server.ts`,build:`bun run --filter @jchat/chat build`,start:`bun apps/chat/server.ts`,"db:generate":`bun --env-file=.env --filter="@jchat/db" run db:generate`,"db:migrate":`bun --env-file=.env --filter="@jchat/db" run db:migrate`,"db:seed":`bun --env-file=.env --filter="@jchat/db" run db:seed`,"db:studio":`bun --env-file=.env --filter="@jchat/db" run db:studio`,"db:push":`bun --env-file=.env --filter="@jchat/db" run db:push`,"db:pull":`bun --env-file=.env --filter="@jchat/db" run db:pull`,"icon:build":`bun run --filter @jchat/icon build`,"ui:build":`bun run --filter @jchat/ui build`,"addon-sdk:build":`bun run icon:build && bun run ui:build && bun run --filter @jchat/addon-sdk build`,"create-addon:build":`bun run --filter @jchat/create-addon build`,"create-addon:test":`bun run --filter @jchat/create-addon test`,"create-addon":`bun run ./packages/create-addon/src/cli.ts`,"runtime:build":`bun run --filter @jchat/runtime build`,"addons:dev":`bun --filter='./addons/*' --parallel run dev`,"addons:init":`bun run ./packages/create-addon/src/cli.ts --workspace`,"addons:sync":`bun run ./scripts/sync-local-addons.ts`,"addons:sandbox":`bun run ./scripts/developer-sandbox.ts run`,"addons:sandbox:init":`bun run ./scripts/developer-sandbox.ts init`,"addons:sandbox:test":`bun run ./scripts/developer-sandbox.ts test`,"addons:sandbox:reset":`bun run ./scripts/developer-sandbox.ts reset`,"addons:sandbox:package":`bun run ./scripts/developer-sandbox.ts package`,"addons:sandbox:cleanup":`bun run ./scripts/developer-sandbox.ts cleanup`,"addons:package":`bun run ./scripts/package-addons.ts`,"addon:release":`bun run ./scripts/release-addon.ts`,"addons:devkit:init":`bun run ./scripts/create-addon-development-kit.ts`,"addons:devkit:artifacts:check":`bun run addon-sdk:build && bun run create-addon:build && bun run ./scripts/validate-addon-development-artifacts.ts`,"store:build":`bun run --filter @jchat/store build`,"store:dev":`bun --watch apps/store/server.ts`,"store:db:generate":`bun --filter @jchat/store db:generate`,"store:db:migrate":`bun --env-file=apps/store/.env --filter @jchat/store db:migrate`,"store:db:seed":`bun --env-file=apps/store/.env --filter @jchat/store db:seed`,"store:test":`bun run --filter @jchat/store test`,"store:contracts:test":`bun run --filter @jchat/store-contracts test`,"license:build":`bun run --filter @jchat/license build`,"license:dev":`bun --watch apps/license/server.ts`,"license:start":`bun run --filter @jchat/license start`,"license:db:generate":`bun --filter @jchat/license db:generate`,"license:db:push":`bun --filter @jchat/license db:push`,"license:test":`bun --env-file=apps/license/.env.test test apps/license`,prepare:`git config core.hooksPath .githooks && bun run addon-sdk:build && bun run runtime:build`,predev:`bun run addon-sdk:build && bun run runtime:build`,release:`bun run ./scripts/release.ts`,"version:addon-sdk":`bun run ./scripts/set-addon-artifact-version.ts sdk`,"version:create-addon":`bun run ./scripts/set-addon-artifact-version.ts creator`,"check:translations":`bun ./scripts/check-translations.ts`,"audit:changes":`bun ./scripts/audit-changes.ts`,"update:geoip":`bun ./scripts/update-geoip.ts`,test:`bun --filter="@jchat/chat" run test:api`,"test:api":`bun --env-file=.env.test --filter="@jchat/chat" run test:api`,"test:unit":`bun --filter="@jchat/chat" run test:unit`,"test:packages":`bun test packages/`,"test:store":`bun run --filter @jchat/store test`,"test:license":`bun --env-file=apps/license/.env.test test apps/license`,"test:watch":`bun --filter="@jchat/chat" run test:api:watch`,"test:coverage":`bun --filter="@jchat/chat" run test:api:coverage`,"test:e2e":`playwright test`,"test:e2e:ui":`playwright test --ui`,"benchmark:chat":`bun run ./scripts/benchmark-chat.ts`,pretypecheck:`bun run addon-sdk:build && bun run runtime:build`,typecheck:`bun --filter='*' run typecheck`,lint:`bun --filter='*' run lint`,format:`bun --filter='*' run format`,change:`bun run ./scripts/release-engine/cli.ts`,"version-packages":`bun run ./scripts/release-engine/version.ts`,"release-packages":`bun run ./scripts/release-engine/publish.ts`,"sync-changelogs":`bun run ./scripts/release-engine/sync-changelog.ts`},devDependencies:{"@clack/prompts":`catalog:`,"@playwright/test":`^1.63.0`,"@types/bun":`catalog:`,elysia:`catalog:`,oxfmt:`catalog:`,oxlint:`catalog:`,typescript:`catalog:`},engines:{node:`>=22.22.0`},packageManager:`bun@1.4.0`,catalog:{"@better-auth/api-key":`^1.7.7`,"@better-auth/passkey":`^1.7.7`,"@casl/ability":`^7.0.1`,"@casl/react":`^7.0.1`,"@clack/prompts":`^1.8.1`,"@daypicker/react":`^10.0.2`,"@elysiajs/cors":`^1.4.1`,"@elysiajs/cron":`^1.4.2`,"@elysiajs/eden":`^1.4.9`,"@elysiajs/static":`^1.4.10`,"@react-router/dev":`^8.4.0`,"@react-router/node":`^8.4.0`,"@react-router/serve":`^8.4.0`,"@tailwindcss/vite":`^4.3.3`,"@tanstack/react-query":`^5.104.0`,"@tanstack/react-virtual":`^3.14.13`,"@types/bun":`^1.4.2`,"@types/nodemailer":`^8.0.2`,"@types/react":`^19.3.0`,"@types/react-dom":`^19.3.0`,"better-auth":`^1.7.7`,cn:`^0.4.0`,"date-fns":`4.4.0`,"drizzle-orm":`^0.45.3`,"drizzle-kit":`^0.31.11`,effect:`^4.0.0`,elysia:`^1.4.30`,"elysia-helmet":`^3.0.0`,"elysia-ip":`^1.1.1`,"elysia-rate-limit":`^4.6.3`,motion:`^13.5.0`,nodemailer:`^10.0.13`,i18next:`^26.4.2`,"i18next-browser-languagedetector":`^8.2.1`,isbot:`^5.2.2`,jotai:`^3.0.1`,kysely:`^0.29.6`,"lucide-react":`1.49.0`,oxfmt:`^0.71.0`,oxlint:`^1.86.0`,react:`^19.3.0`,"react-dom":`^19.3.0`,"react-hook-form":`7.54.2`,"react-hot-toast":`^2.6.1`,"react-i18next":`^17.0.15`,"react-router":`^8.4.0`,"tailwind-scrollbar":`^4.0.2`,tailwindcss:`^4.3.3`,tsdown:`0.23.0`,typescript:`^7.0.2`,vite:`^8.3.2`,zod:`^4.6.5`},trustedDependencies:[`@tailwindcss/oxide`,`esbuild`,`unrs-resolver`]};function x(){return b&&`catalog`in b&&typeof b.catalog==`object`&&b.catalog!==null?b.catalog:{}}function S(e){return{name:`${e}-development-kit`,private:!0,type:`module`,workspaces:[`addon`],scripts:{sandbox:`bun run ./scripts/devkit.ts run`,"sandbox:test":`bun run ./scripts/devkit.ts test`,"sandbox:reset":`bun run ./scripts/devkit.ts reset`,"sandbox:package":`bun run ./scripts/devkit.ts package`,"sandbox:cleanup":`bun run ./scripts/devkit.ts cleanup`},devDependencies:{"@types/bun":`^1.4.0`},engines:{bun:`>=1.4.0`}}}function C(e,t){return{name:`@jchat/${e}`,version:`1.0.0-alpha.0`,private:!0,description:`${t} addon for JChat`,repository:{type:`git`,url:`git+https://github.com/jchatcloud/jchat.git`,directory:`addons/${e}`},type:`module`,scripts:{dev:`jchat-addon dev`,build:`tsc --noEmit && jchat-addon build`,test:`bun test`,lint:`oxlint .`,format:`oxfmt . --write`,typecheck:`tsc --noEmit`},dependencies:{},devDependencies:{"@tailwindcss/vite":`catalog:`,"@types/bun":`catalog:`,"@types/react":`catalog:`,"@types/react-dom":`catalog:`,elysia:`catalog:`,vite:`catalog:`},peerDependencies:{"@jchat/addon-sdk":`workspace:*`,"@tanstack/react-query":`catalog:`,react:`catalog:`,"react-dom":`catalog:`}}}function w(e,t){let n=x();return{name:`@jchat/${e}`,version:`1.0.0`,private:!0,type:`module`,scripts:{dev:`jchat-addon dev`,build:`tsc --noEmit && jchat-addon build`,test:`bun test`,typecheck:`tsc --noEmit`},dependencies:{},devDependencies:{"@tailwindcss/vite":n[`@tailwindcss/vite`]??`^4.3.3`,"@types/bun":n[`@types/bun`]??`^1.4.0`,"@types/react":n[`@types/react`]??`^19.2.18`,"@types/react-dom":n[`@types/react-dom`]??`^19.2.4`,elysia:n.elysia??`^1.4.29`,tailwindcss:n.tailwindcss??`^4.3.3`,vite:n.vite??`^8.2.1`},peerDependencies:{"@jchat/addon-sdk":`^${t}`,"@tanstack/react-query":n[`@tanstack/react-query`]??`^5.101.4`,react:n.react??`^19.2.8`,"react-dom":n[`react-dom`]??`^19.2.8`}}}function T(){return{extends:`@jchat/addon-sdk/tsconfig.json`,include:[`src/**/*`]}}function E(){return{extends:`@jchat/addon-sdk/tsconfig.json`,include:[`src/**/*.ts`,`src/**/*.tsx`]}}function D(){return`---
464
+ name: jchat-addon-development
465
+ description: Comprehensive guide for developing JChat addons using @jchat/addon-sdk, including manifests, UI widgets, direct icon imports, Elysia backend, isolated DB schemas, and i18n.
466
+ ---
467
+
468
+ # JChat Addon Development Skill
469
+
470
+ Use this skill when developing, updating, or debugging JChat addons using \`@jchat/addon-sdk\`.
471
+
472
+ ---
473
+
474
+ ## 1. Architectural Guardrails (Non-Negotiable)
475
+
476
+ 1. **Import Boundaries**:
477
+ - Addons MUST ONLY import \`@jchat/addon-sdk\`, \`@jchat/addon-sdk/ui\` (components & toast), \`@jchat/addon-sdk/state\` (atomic state), \`@jchat/addon-sdk/icon\`, and \`@jchat/addon-sdk/server\`.
478
+ - NEVER import private internal monorepo packages (\`@jchat/icon\`, \`@jchat/ui\`, \`@jchat/db\`, \`@jchat/shared\`, etc.).
479
+ - Host externals (\`react\`, \`react-dom\`, \`@tanstack/react-query\`) are provided at runtime by the JChat host.
480
+ 2. **Icons**:
481
+ - Use direct imports: \`import Icons from "@jchat/addon-sdk/icon"\`.
482
+ - Never call \`useIcons()\` or install \`lucide-react\` / \`react-icons\` directly in the addon.
483
+ 3. **Database Isolation**:
484
+ - All addon tables MUST reside in the isolated \`addons\` PostgreSQL schema (e.g. \`addons.GlimpseStories\`).
485
+ - Never write to or alter core tables (\`Users\`, \`Rooms\`, \`Messages\`, \`Sites\`).
486
+ 4. **Real-Time Event Namespacing**:
487
+ - All WebSocket/Redis events MUST follow the canonical format: \`addon:<addonId>:<eventName>\` (e.g. \`addon:pollwise:voted\`).
488
+ 5. **Localization (i18n)**:
489
+ - All addons must support all 20 host languages:
490
+ \`ar\`, \`bg\`, \`de\`, \`el\`, \`en\`, \`es\`, \`fr\`, \`he\`, \`hi\`, \`hr\`, \`kn\`, \`ml\`, \`nl\`, \`pt\`, \`ro\`, \`ru\`, \`ta\`, \`te\`, \`tr\`, \`ur\`.
491
+ - All locale dictionaries in \`src/locales/*.json\` must maintain 100% key parity.
492
+ 6. **Directional Styling (RTL/LTR)**:
493
+ - Use Tailwind CSS logical properties (\`ps-*\`, \`pe-*\`, \`ms-*\`, \`me-*\`, \`text-start\`, \`text-end\`, \`start-*\`, \`end-*\`, \`rounded-s-*\`, \`rounded-e-*\`).
494
+ - Never use physical \`pl-*\`, \`pr-*\`, \`ml-*\`, \`mr-*\`, \`text-left\`, or \`text-right\`.
495
+
496
+ ---
497
+
498
+ ## 2. Manifest Contract (\`src/manifest.ts\`)
499
+
500
+ Define addon metadata and capabilities with \`defineAddonManifest\`:
501
+
502
+ \`\`\`typescript
503
+ import { defineAddonManifest } from "@jchat/addon-sdk"
504
+
505
+ export default defineAddonManifest({
506
+ id: "pollwise",
507
+ name: "Pollwise Polls",
508
+ version: "1.0.0",
509
+ description: "Create and vote on live interactive polls in chat rooms.",
510
+ author: "JChat Ecosystem Team",
511
+ compatibility: {
512
+ jchat: ">=1.0.0-alpha.0 <2.0.0",
513
+ addonSdk: ">=1.0.0-alpha.0 <2.0.0"
514
+ },
515
+ locales: [
516
+ "ar", "bg", "de", "el", "en", "es", "fr", "he", "hi", "hr",
517
+ "kn", "ml", "nl", "pt", "ro", "ru", "ta", "te", "tr", "ur"
518
+ ],
519
+ defaultLocale: "en",
520
+ frontend: {
521
+ bundle: "./ui.js",
522
+ mountPoints: ["chat-toolbar", "room-settings"]
523
+ },
524
+ backend: {
525
+ bundle: "./server.js"
526
+ },
527
+ permissions: ["network", "storage"]
528
+ })
529
+ \`\`\`
530
+
531
+ ---
532
+
533
+ ## 3. Frontend UI Development (\`src/ui.tsx\`)
534
+
535
+ Addon UI components receive \`AddonProps\` and use hooks from \`@jchat/addon-sdk\`:
536
+
537
+ \`\`\`tsx
538
+ import {
539
+ type AddonProps,
540
+ useAddonSDK,
541
+ useAddonQuery,
542
+ useAddonMutation,
543
+ useAddonTranslation
544
+ } from "@jchat/addon-sdk"
545
+ import Icons from "@jchat/addon-sdk/icon"
546
+
547
+ export default function PollwiseAddon({ mountPoint, roomId }: AddonProps) {
548
+ const sdk = useAddonSDK()
549
+ const { t } = useAddonTranslation()
550
+
551
+ // 1. Data fetching via TanStack Query bridge
552
+ const { data: poll, isLoading } = useAddonQuery({
553
+ queryKey: ["poll", roomId],
554
+ queryFn: () => sdk.api.get<{ id: string; question: string }>(\`/rooms/\${roomId}/poll\`)
555
+ })
556
+
557
+ // 2. Mutations
558
+ const voteMutation = useAddonMutation({
559
+ mutationFn: (optionId: string) => sdk.api.post(\`/rooms/\${roomId}/vote\`, { optionId }),
560
+ onSuccess: () => {
561
+ sdk.ui.toast({
562
+ title: t("vote_success_title"),
563
+ description: t("vote_success_desc"),
564
+ variant: "success"
565
+ })
566
+ }
567
+ })
568
+
569
+ // 3. Real-time event listener
570
+ useEffect(() => {
571
+ const unsubscribe = sdk.events.on("addon:pollwise:updated", (updatedPoll) => {
572
+ // Handle incoming real-time payload
573
+ })
574
+ return () => unsubscribe()
575
+ }, [sdk])
576
+
577
+ if (isLoading) return <div>{t("loading")}</div>
578
+
579
+ return (
580
+ <div className="p-4 rounded-xl bg-card border border-border">
581
+ <div className="flex items-center gap-2 mb-3">
582
+ <Icons.BarChart3 className="size-5 text-primary" />
583
+ <h3 className="font-semibold text-foreground">{poll?.question}</h3>
584
+ </div>
585
+ <button
586
+ onClick={() => voteMutation.mutate("option-1")}
587
+ className="px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
588
+ >
589
+ {t("vote_button")}
590
+ </button>
591
+ </div>
592
+ )
404
593
  }
405
- function toDisplayName(addonId) {
406
- return addonId.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
594
+ \`\`\`
595
+
596
+ ### Direct Icon Usage
597
+ Icons are rendered directly:
598
+ \`\`\`tsx
599
+ import Icons, { resolveIcon } from "@jchat/addon-sdk/icon"
600
+
601
+ // Direct JSX:
602
+ <Icons.Sparkles className="size-4 text-primary" />
603
+ <Icons.Send className="size-5" />
604
+
605
+ // Dynamic resolution:
606
+ const DynamicIcon = resolveIcon("Smile", Icons.HelpCircle)
607
+ <DynamicIcon className="size-4" />
608
+ \`\`\`
609
+
610
+ ---
611
+
612
+ ## 4. Backend Server Development (\`src/server.ts\`)
613
+
614
+ Fullstack addons expose ElysiaJS routes running within the JChat server runtime:
615
+
616
+ \`\`\`typescript
617
+ import { createAddonServer } from "@jchat/addon-sdk/server"
618
+ import { t } from "elysia"
619
+
620
+ export default createAddonServer("pollwise", ({ app, db, events }) => {
621
+ return app
622
+ .get("/rooms/:roomId/poll", async ({ params: { roomId } }) => {
623
+ // Query isolated addons schema using Kysely
624
+ const poll = await db
625
+ .selectFrom("addons.Polls")
626
+ .selectAll()
627
+ .where("roomId", "=", roomId)
628
+ .executeTakeFirst()
629
+
630
+ return poll ?? { message: "No active poll" }
631
+ })
632
+ .post(
633
+ "/rooms/:roomId/vote",
634
+ async ({ params: { roomId }, body, user }) => {
635
+ // Record vote in DB
636
+ await db
637
+ .insertInto("addons.PollVotes")
638
+ .values({
639
+ pollId: body.pollId,
640
+ userId: user.id,
641
+ optionId: body.optionId
642
+ })
643
+ .execute()
644
+
645
+ // Broadcast real-time event to room subscribers
646
+ await events.publish(\`addon:pollwise:updated\`, {
647
+ roomId,
648
+ pollId: body.pollId
649
+ })
650
+
651
+ return { success: true }
652
+ },
653
+ {
654
+ body: t.Object({
655
+ pollId: t.String(),
656
+ optionId: t.String()
657
+ })
658
+ }
659
+ )
660
+ })
661
+ \`\`\`
662
+
663
+ ---
664
+
665
+ ## 5. Addon Localization (\`src/locales/\`)
666
+
667
+ Every translatable string in the UI must use \`t("key")\`.
668
+ Translations reside in \`src/locales/<lang>.json\` for all 20 supported languages:
669
+
670
+ \`\`\`json
671
+ // src/locales/en.json
672
+ {
673
+ "welcome": "Welcome to Pollwise",
674
+ "vote_button": "Submit Vote",
675
+ "vote_success_title": "Vote Recorded",
676
+ "vote_success_desc": "Thank you for participating."
407
677
  }
408
- //#endregion
409
- export { writeStandaloneDevkit as n, createStandaloneDevkitFiles as t };
678
+ \`\`\`
679
+
680
+ Validate key parity across all 20 languages:
681
+ \`\`\`bash
682
+ bun check:translations
683
+ \`\`\`
684
+
685
+ ---
686
+
687
+ ## 6. Build and Verification Workflow
688
+
689
+ \`\`\`bash
690
+ # 1. Typecheck the addon
691
+ bun run typecheck
692
+
693
+ # 2. Bundle the addon artifacts (frontend bundle into ui.js, backend into server.js)
694
+ bun run build
695
+
696
+ # 3. In the standalone devkit: start the disposable sandbox environment
697
+ bun run devkit:start
698
+
699
+ # 4. View sandbox logs
700
+ bun run devkit:logs
701
+ \`\`\`
702
+ `}async function O(e,t,i=`Target already exists`){let a=r.resolve(e);if(n.existsSync(a))throw Error(`${i}: ${a}`);let o=Object.entries(t);for(let[e]of o){let t=r.join(a,...e.split(`/`));n.mkdirSync(r.dirname(t),{recursive:!0})}await Promise.all(o.map(([e,t])=>{let n=r.join(a,...e.split(`/`));return Bun.write(n,t)}))}const k=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;function A(e){return e.split(`-`).map(e=>`${e[0]?.toUpperCase()??``}${e.slice(1)}`).join(` `)}function j(n){if(!k.test(n.addonId))throw Error(`Addon id must contain lowercase letters, numbers, and hyphens`);let r=(n.template??`ui`)===`fullstack`,l=n.displayName?.trim()||A(n.addonId),u=n.mountPoints&&n.mountPoints.length>0?n.mountPoints:[e.CHAT_TOOLBAR,e.CHAT_PLUS_MENU],d={id:n.addonId,name:l,version:`1.0.0`,description:`${l} addon for JChat`,logo:`./assets/logo.svg`,author:`Developer`,compatibility:{jchat:i(n.jchatVersion),addonSdk:i(n.addonSdkVersion)},locales:[...t],defaultLocale:`en`,frontend:{bundle:`./ui.js`,mountPoints:u},...r?{server:{bundle:`./api.ts`,migrations:[`migrations/001_initial.sql`]},permissions:[`addon.${n.addonId}.use`,`addon.${n.addonId}.manage`],permissionDefinitions:[{name:`addon.${n.addonId}.use`,label:`Use ${l}`,description:`Access and use ${l} in chat`},{name:`addon.${n.addonId}.manage`,label:`Manage ${l}`,description:`Configure and moderate ${l} settings`}]}:{permissions:[]}},f=n.isMonorepoWorkspace?C(n.addonId,l):w(n.addonId,n.addonSdkVersion),p=n.isMonorepoWorkspace?T():E(),m={"package.json":`${JSON.stringify(f,null,2)}\n`,"tsconfig.json":`${JSON.stringify(p,null,2)}\n`,"assets/logo.svg":_(l),"src/manifest.ts":v(d),"src/tailwind.css":y(),...g(l),"src/ui.tsx":c(l,u,r),".agents/skills/jchat-addon-development/SKILL.md":D()};return r&&(m[`src/db.ts`]=a(n.addonId),m[`src/api.ts`]=s(n.addonId),m[`migrations/001_initial.sql`]=o(n.addonId)),Object.freeze(m)}async function M(e){await O(e.targetDir,j(e),`Addon target already exists`)}function N(e){if(!k.test(e.addonId))throw Error(`Addon id must contain lowercase letters, numbers, and hyphens`);let t=e.sandboxImage??`ghcr.io/jchatcloud/jchat-sandbox:${e.jchatVersion.replace(/\+/g,`-`)}`,n=e.displayName?.trim()||A(e.addonId),r=j({addonId:e.addonId,isMonorepoWorkspace:!1,jchatVersion:e.jchatVersion,addonSdkVersion:e.addonSdkVersion,template:e.template,mountPoints:e.mountPoints,displayName:e.displayName}),i={".env.example":l(e.addonId,t),".gitignore":[`node_modules/`,`addon/node_modules/`,`addon/build/`,`.env`,``].join(`
703
+ `),"README.md":f(e.addonId,n),"docker-compose.yml":u(),"package.json":`${JSON.stringify(S(e.addonId),null,2)}\n`,"scripts/devkit.ts":d(),".agents/skills/jchat-addon-development/SKILL.md":D()};for(let[e,t]of Object.entries(r))i[`addon/${e}`]=t;return Object.freeze(i)}async function P(e){await O(e.targetDir,N(e),`Development kit target already exists`)}export{P as a,M as i,N as n,A as r,j as t};