@jchat/create-addon 1.0.0-alpha.11 → 1.0.0-alpha.13
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/CHANGELOG.md +17 -0
- package/README.md +168 -25
- package/dist/cli.d.mts +11 -5
- package/dist/cli.mjs +71 -1
- package/dist/contracts.d.mts +40 -0
- package/dist/generator.d.mts +39 -0
- package/dist/generator.mjs +497 -14
- package/dist/index.d.mts +3 -26
- package/dist/index.mjs +1 -1
- package/package.json +18 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @jchat/create-addon
|
|
2
2
|
|
|
3
|
+
## 1.0.0-alpha.13
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 1.0.0-alpha.12
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 3d4d0ad: - Add interactive terminal UI powered by `@clack/prompts` with automatic TTY detection and positional argument support
|
|
12
|
+
- Add full-stack addon template scaffolding with Elysia API router, Kysely database schema, SQL migrations, and custom permission definitions
|
|
13
|
+
- Add multi-mountpoint support with dynamic UI rendering and directional RTL/LTR styles
|
|
14
|
+
- Expand locale scaffolding to all 20 host languages with 100% key parity and automated synchronization testing
|
|
15
|
+
- Strongly type `MountPoint` and `MOUNT_POINTS` contracts across generator and CLI interfaces
|
|
16
|
+
- Bundle contracts inline in `@jchat/create-addon` for standalone, zero-dependency CLI execution
|
|
17
|
+
- Export `./contracts` subpath from `@jchat/addon-sdk` for lightweight, framework-agnostic contract imports
|
|
18
|
+
- 7b86222: Add official customer-facing Agent Skills for JChat addon development and creation. Bundles `@jchat/addon-sdk` skills (`jchat-addon-development` and `create-jchat-addon`) for AI coding assistants and automatically scaffolds in-workspace skills when creating new addon projects.
|
|
19
|
+
|
|
3
20
|
## 1.0.0-alpha.11
|
|
4
21
|
|
|
5
22
|
## 1.0.0-alpha.10
|
package/README.md
CHANGED
|
@@ -1,68 +1,211 @@
|
|
|
1
1
|
# @jchat/create-addon
|
|
2
2
|
|
|
3
|
-
Official CLI scaffolding tool for generating self-contained [JChat](https://
|
|
3
|
+
Official CLI scaffolding tool for generating self-contained [JChat](https://jchatlive.com) addon development workspaces and interactive sandbox environments.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### 1. Interactive Scaffolding (Recommended)
|
|
10
|
+
|
|
11
|
+
Run without flags in a terminal for an interactive, guided setup powered by `@clack/prompts`:
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
bunx @jchat/create-addon
|
|
14
|
+
bunx @jchat/create-addon
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
The interactive wizard will guide you through:
|
|
18
|
+
|
|
19
|
+
1. **Addon ID**: URL-friendly slug (e.g., `poll-manager`).
|
|
20
|
+
2. **Display Name**: Human-readable name (e.g., `Poll Manager`).
|
|
21
|
+
3. **Template**: Full-Stack (UI + Backend API + DB) or Frontend Only.
|
|
22
|
+
4. **Mount Points**: Where the addon attaches in the host UI.
|
|
23
|
+
5. **Target Environment**: Standalone DevKit (Docker Sandbox) or Monorepo Workspace.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### 2. Non-Interactive / Scripted
|
|
28
|
+
|
|
29
|
+
Scaffold immediately by providing flags:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Standalone DevKit with full-stack template
|
|
33
|
+
bunx @jchat/create-addon poll-manager --template=fullstack --name="Poll Manager"
|
|
34
|
+
|
|
35
|
+
# Inside the JChat monorepo (outputs to addons/poll-manager)
|
|
36
|
+
bunx @jchat/create-addon poll-manager --workspace --template=fullstack
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Development Workflows
|
|
42
|
+
|
|
43
|
+
### Standalone DevKit (Independent Repository)
|
|
44
|
+
|
|
45
|
+
When building an addon outside the JChat core monorepo, `@jchat/create-addon` scaffolds a complete DevKit featuring an isolated containerized sandbox (PostgreSQL 18, Redis 8, HTTP Mock server, and JChat sandbox runtime):
|
|
16
46
|
|
|
17
47
|
```bash
|
|
18
|
-
bunx @jchat/create-addon poll-manager
|
|
19
48
|
cd poll-manager
|
|
20
49
|
|
|
21
|
-
#
|
|
50
|
+
# 1. Initialize environment config
|
|
51
|
+
cp .env.example .env
|
|
52
|
+
|
|
53
|
+
# 2. Install dependencies
|
|
54
|
+
bun install
|
|
55
|
+
|
|
56
|
+
# 3. Start the local sandbox container environment
|
|
57
|
+
bun sandbox
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Standalone DevKit Automation Scripts
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
| :-------------------- | :-------------------------------------------------------------------------- |
|
|
64
|
+
| `bun sandbox` | Boots Docker/Podman sandbox with live reload, database, and HTTP mock proxy |
|
|
65
|
+
| `bun sandbox:test` | Executes integration and unit tests within the sandbox container |
|
|
66
|
+
| `bun sandbox:package` | Builds and packages the addon into a redistributable `.zip` archive |
|
|
67
|
+
| `bun sandbox:reset` | Resets database fixtures and clears sandbox upload storage |
|
|
68
|
+
| `bun sandbox:cleanup` | Stops and removes all sandbox containers and networks |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Monorepo Workspace (`--workspace`)
|
|
73
|
+
|
|
74
|
+
When developing locally inside the JChat monorepo:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Scaffold into addons/<addon-id>
|
|
78
|
+
bunx @jchat/create-addon my-addon --workspace
|
|
79
|
+
|
|
80
|
+
# Start live development with hot reload
|
|
81
|
+
cd addons/my-addon
|
|
22
82
|
bun dev
|
|
23
83
|
```
|
|
24
84
|
|
|
85
|
+
#### Addon Workspace Scripts
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
| :-------------- | :------------------------------------------------------------------ |
|
|
89
|
+
| `bun dev` | Starts `jchat-addon dev` with fast HMR |
|
|
90
|
+
| `bun build` | Runs typechecking (`tsc --noEmit`) and compiles `jchat-addon build` |
|
|
91
|
+
| `bun test` | Runs Bun unit tests and verifies 20-language translation parity |
|
|
92
|
+
| `bun typecheck` | Validates TypeScript contracts without emitting output |
|
|
93
|
+
|
|
25
94
|
---
|
|
26
95
|
|
|
27
96
|
## Workspace Structure
|
|
28
97
|
|
|
29
|
-
|
|
98
|
+
### Full-Stack Addon Template (`--template=fullstack`)
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
my-addon/
|
|
102
|
+
├── addon/
|
|
103
|
+
│ ├── migrations/
|
|
104
|
+
│ │ └── 001_initial.sql # Kysely/SQL database schema migrations
|
|
105
|
+
│ ├── src/
|
|
106
|
+
│ │ ├── locales/ # All 20 host languages
|
|
107
|
+
│ │ │ ├── en.json
|
|
108
|
+
│ │ │ ├── ... (20 languages)
|
|
109
|
+
│ │ │ └── index.ts
|
|
110
|
+
│ │ ├── locales.test.ts # Key-parity tests across all 20 languages
|
|
111
|
+
│ │ ├── api.ts # Backend ElysiaJS routes & business logic
|
|
112
|
+
│ │ ├── db.ts # Kysely database helper & typed tables
|
|
113
|
+
│ │ ├── manifest.ts # Addon permissions, metadata & mount points
|
|
114
|
+
│ │ ├── tailwind.css # Scoped Tailwind CSS v4 styling
|
|
115
|
+
│ │ └── ui.tsx # Frontend React UI component
|
|
116
|
+
│ ├── package.json
|
|
117
|
+
│ └── tsconfig.json
|
|
118
|
+
├── .agents/
|
|
119
|
+
│ └── skills/
|
|
120
|
+
│ └── jchat-addon-development/ # AI pair-programming skill for the addon
|
|
121
|
+
├── .env.example # Sandbox configuration & ports
|
|
122
|
+
├── docker-compose.yml # Containerized PostgreSQL 18, Redis 8 & sandbox
|
|
123
|
+
├── package.json # DevKit root lifecycle scripts
|
|
124
|
+
├── README.md # Project documentation
|
|
125
|
+
└── scripts/
|
|
126
|
+
└── devkit.ts # Standalone sandbox orchestrator
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Frontend-Only Addon Template (`--template=ui`)
|
|
130
|
+
|
|
131
|
+
Lightweight UI-only extension without server routes or database tables:
|
|
30
132
|
|
|
31
133
|
```text
|
|
32
134
|
my-addon/
|
|
33
135
|
├── addon/
|
|
34
136
|
│ ├── src/
|
|
35
|
-
│ │ ├──
|
|
36
|
-
│ │ ├──
|
|
37
|
-
│ │ ├──
|
|
38
|
-
│ │
|
|
137
|
+
│ │ ├── locales/ # 20 host language translations
|
|
138
|
+
│ │ ├── locales.test.ts # Key parity test
|
|
139
|
+
│ │ ├── manifest.ts # Client mount points & metadata
|
|
140
|
+
│ │ ├── tailwind.css # Scoped Tailwind CSS
|
|
141
|
+
│ │ └── ui.tsx # React UI component
|
|
39
142
|
│ ├── package.json
|
|
40
|
-
│
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
├── package.json # Workspace automation scripts
|
|
44
|
-
└── README.md
|
|
143
|
+
│ └── tsconfig.json
|
|
144
|
+
├── docker-compose.yml
|
|
145
|
+
└── package.json
|
|
45
146
|
```
|
|
46
147
|
|
|
47
148
|
---
|
|
48
149
|
|
|
49
|
-
## CLI Options
|
|
150
|
+
## CLI Flags & Options
|
|
151
|
+
|
|
152
|
+
| Option | Description | Default |
|
|
153
|
+
| :----------------------- | :----------------------------------------------------------------------------------------- | :----------------------------------- |
|
|
154
|
+
| `<addon-id>` | Addon identifier (lowercase letters, numbers, hyphens). Prompted interactively if omitted. | — |
|
|
155
|
+
| `--target=<dir>` | Output directory for the scaffolded project. | `./<addon-id>` |
|
|
156
|
+
| `--template=<type>` | Addon template type: `fullstack` (UI + API + DB) or `ui` (Frontend only). | `fullstack` |
|
|
157
|
+
| `--mount-points=<p1,p2>` | Comma-separated list of UI mount points (see below). | `chat:toolbar,chat:plus-menu` |
|
|
158
|
+
| `--name=<displayName>` | Human-readable display name for the addon (e.g. `--name="Poll Manager"`). | Auto-derived from `<addon-id>` |
|
|
159
|
+
| `--workspace` | Target monorepo workspace mode (scaffolds to `addons/<addon-id>`). | `false` |
|
|
160
|
+
| `--image=<image>` | Custom container image for the JChat sandbox runtime. | `ghcr.io/mjjabarullah/jchat-sandbox` |
|
|
161
|
+
| `--help`, `-h` | Display CLI usage and options. | — |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Supported Mount Points
|
|
166
|
+
|
|
167
|
+
Addons mount into specific extension slots across the host application:
|
|
168
|
+
|
|
169
|
+
| Mount Point | Description |
|
|
170
|
+
| :------------------------- | :---------------------------------------------------------- |
|
|
171
|
+
| `chat:toolbar` | Header toolbar button in the active room |
|
|
172
|
+
| `chat:plus-menu` | Action item inside the message composer plus (`+`) menu |
|
|
173
|
+
| `chat:sidebar` | Right-hand widget panel in chat |
|
|
174
|
+
| `addon:settings` | Addon configuration panel in site administration dashboard |
|
|
175
|
+
| `owner:settings` | Addon configuration panel in owner administration dashboard |
|
|
176
|
+
| `emoji-picker:gif-tab` | Custom tab inside the chat emoji / GIF picker panel |
|
|
177
|
+
| `emoji-picker:sticker-tab` | Custom tab inside the chat sticker picker panel |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Addon SDK Subpath Architecture
|
|
182
|
+
|
|
183
|
+
Generated addons import exclusively from `@jchat/addon-sdk` subpaths:
|
|
184
|
+
|
|
185
|
+
- **`@jchat/addon-sdk`**: Manifest definitions, backend Elysia server utilities, permissions, contracts, and lifecycle types.
|
|
186
|
+
- **`@jchat/addon-sdk/ui`**: Base UI component library, design tokens, and unified `toast` notifications (`import { Button, toast } from "@jchat/addon-sdk/ui"`).
|
|
187
|
+
- **`@jchat/addon-sdk/icon`**: Centralized icon pack with direct TypeScript definitions (`import Icons from "@jchat/addon-sdk/icon"`).
|
|
188
|
+
- **`@jchat/addon-sdk/state`**: Shared atomic state powered by Jotai (`import { atom, useAtom } from "@jchat/addon-sdk/state"`).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 20-Language Localization Out of the Box
|
|
193
|
+
|
|
194
|
+
All scaffolded addons automatically include string definitions for all 20 host languages supported by JChat:
|
|
195
|
+
|
|
196
|
+
`ar`, `bg`, `de`, `el`, `en`, `es`, `fr`, `he`, `hi`, `hr`, `kn`, `ml`, `nl`, `pt`, `ro`, `ru`, `ta`, `te`, `tr`, `ur`
|
|
50
197
|
|
|
51
|
-
|
|
52
|
-
| :---------------- | :------------------------------------------------------------------ | :----------------------------------- |
|
|
53
|
-
| `<addon-id>` | **Required**. Unique identifier (lowercase alphanumeric & hyphens). | — |
|
|
54
|
-
| `--target=<dir>` | Target output directory for the scaffolded workspace. | `./<addon-id>` |
|
|
55
|
-
| `--image=<image>` | Specific container image for the JChat sandbox runtime. | `ghcr.io/mjjabarullah/jchat-sandbox` |
|
|
198
|
+
A built-in test (`src/locales.test.ts`) verifies 100% key parity across all languages on every `bun test`.
|
|
56
199
|
|
|
57
200
|
---
|
|
58
201
|
|
|
59
|
-
##
|
|
202
|
+
## Prerequisites
|
|
60
203
|
|
|
61
204
|
- **Bun**: 1.4.0+
|
|
62
|
-
- **Docker** or **Podman
|
|
205
|
+
- **Docker** or **Podman**: Required for running the local standalone sandbox environment (`bun sandbox`).
|
|
63
206
|
|
|
64
207
|
---
|
|
65
208
|
|
|
66
209
|
## License
|
|
67
210
|
|
|
68
|
-
Proprietary — All rights reserved. See the [LICENSE](./LICENSE) file
|
|
211
|
+
Proprietary — All rights reserved. See the [LICENSE](./LICENSE) file for terms and conditions.
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { n as MountPoint } from "./contracts.mjs";
|
|
2
|
+
import { t as AddonTemplateType } from "./generator.mjs";
|
|
1
3
|
//#region src/cli.d.ts
|
|
2
|
-
interface CreatorArguments {
|
|
4
|
+
export interface CreatorArguments {
|
|
3
5
|
addonId: string;
|
|
4
6
|
targetDir: string;
|
|
5
7
|
sandboxImage?: string;
|
|
6
8
|
workspace?: boolean;
|
|
9
|
+
template?: AddonTemplateType;
|
|
10
|
+
mountPoints?: MountPoint[];
|
|
11
|
+
displayName?: string;
|
|
7
12
|
}
|
|
8
|
-
declare function parseCreatorArguments(arguments_: readonly string[]): CreatorArguments;
|
|
9
|
-
declare function
|
|
10
|
-
|
|
11
|
-
export
|
|
13
|
+
export declare function parseCreatorArguments(arguments_: readonly string[]): CreatorArguments;
|
|
14
|
+
export declare function isInsideJChatMonorepo(): boolean;
|
|
15
|
+
export declare function promptInteractiveOptions(initialAddonId?: string): Promise<CreatorArguments>;
|
|
16
|
+
export declare function runCreator(arguments_: readonly string[]): Promise<string>;
|
|
17
|
+
//#endregion
|
package/dist/cli.mjs
CHANGED
|
@@ -1,2 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import{
|
|
2
|
+
import{t as e}from"./contracts.d.mts";import{a as t,i as n,o as r}from"./generator.mjs";import*as i from"node:fs";import*as a from"node:path";import{styleText as o}from"node:util";import s,{stdin as c,stdout as l}from"node:process";import*as u from"node:readline";import d from"node:readline";import{ReadStream as f}from"node:tty";var p=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports);const m=(()=>{let e=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g;return t=>{let n=0;for(e.lastIndex=0;e.test(t);)n+=1;return t.length-n}})(),h=e=>e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510,g=e=>e===8987||e===9001||e>=12272&&e<=12287||e>=12289&&e<=12350||e>=12441&&e<=12543||e>=12549&&e<=12591||e>=12593&&e<=12686||e>=12688&&e<=12771||e>=12783&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=19903||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=127488&&e<=127490||e>=127504&&e<=127547||e>=127552&&e<=127560||e>=131072&&e<=196605||e>=196608&&e<=262141,_=/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y,v=/[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y,y=/(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/uy,b=/\t{1,1000}/y,x=/[\u{1F1E6}-\u{1F1FF}]{2}|\u{1F3F4}[\u{E0061}-\u{E007A}]{2}[\u{E0030}-\u{E0039}\u{E0061}-\u{E007A}]{1,3}\u{E007F}|(?:\p{Emoji}\uFE0F\u20E3?|\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation})(?:\u200D(?:\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F\u20E3?))*/uy,S=/(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y,ee=/\p{M}+/gu,te={limit:1/0,ellipsis:``},C=(e,t={},n={})=>{let r=t.limit??1/0,i=t.ellipsis??``,a=t?.ellipsisWidth??(i?C(i,te,n).width:0),o=n.controlWidth??0,s=n.tabWidth??8,c=n.emojiWidth??2,l=n.regularWidth??1,u=n.wideWidth??2,d=[[S,l],[_,0],[v,o],[b,s],[x,c],[y,u]],f=0,p=0,w=e.length,T=0,E=!1,D=w,O=Math.max(0,r-a),k=0,A=0,j=0,M=0;outer:for(;;){if(A>k||p>=w&&p>f){let t=e.slice(k,A)||e.slice(f,p);T=0;for(let e of t.replaceAll(ee,``)){let t=e.codePointAt(0)||0;if(M=h(t)?2:g(t)?u:l,j+M>O&&(D=Math.min(D,Math.max(k,f)+T)),j+M>r){E=!0;break outer}T+=e.length,j+=M}k=A=0}if(p>=w)break outer;for(let t=0,n=d.length;t<n;t++){let[n,i]=d[t];if(n.lastIndex=p,n.test(e)){if(T=n===y?m(e.slice(p,n.lastIndex)):n===x?1:n.lastIndex-p,M=T*i,j+M>O&&(D=Math.min(D,p+Math.floor((O-j)/i))),j+M>r){E=!0;break outer}j+=M,k=f,A=p,p=f=n.lastIndex;continue outer}}p+=1}return{width:E?O:j,index:E?D:w,truncated:E,ellipsed:E&&r>=a}},w={limit:1/0,ellipsis:``,ellipsisWidth:0},T=(e,t={})=>C(e,w,t).width,E=`]8;;`,D=RegExp(`(?:\\[(?<code>\\d+)m|\\${E}(?<uri>.*))`,`y`),O=e=>{if(e>=30&&e<=37||e>=90&&e<=97)return 39;if(e>=40&&e<=47||e>=100&&e<=107)return 49;if(e===1||e===2)return 22;if(e===3)return 23;if(e===4)return 24;if(e===7)return 27;if(e===8)return 28;if(e===9)return 29;if(e===0)return 0},k=e=>`[${e}m`,A=e=>`${E}${e}`,j=(e,t,n)=>{let r=t[Symbol.iterator](),i=!1,a=!1,o=e.at(-1),s=o===void 0?0:T(o),c=r.next(),l=r.next(),u=0;for(;!c.done;){let o=c.value,d=T(o);s+d<=n?e[e.length-1]+=o:(e.push(o),s=0),(o===`\x1B`||o===``)&&(i=!0,a=t.startsWith(E,u+1)),i?a?o===`\x07`&&(i=!1,a=!1):o===`m`&&(i=!1):(s+=d,s===n&&!l.done&&(e.push(``),s=0)),c=l,l=r.next(),u+=o.length}o=e.at(-1),!s&&o!==void 0&&o.length&&e.length>1&&(e[e.length-2]+=e.pop())},M=e=>{let t=e.split(` `),n=t.length;for(;n&&!T(t[n-1]);)n--;return n===t.length?e:t.slice(0,n).join(` `)+t.slice(n).join(``)},ne=(e,t,n={})=>{if(n.trim!==!1&&e.trim()===``)return``;let r=``,i,a,o=e.split(` `),s=[``],c=0;for(let e=0;e<o.length;e++){let r=o[e];if(n.trim!==!1){let e=s.at(-1)??``,t=e.trimStart();e.length!==t.length&&(s[s.length-1]=t,c=T(t))}e!==0&&(c>=t&&(n.wordWrap===!1||n.trim===!1)&&(s.push(``),c=0),(c||n.trim===!1)&&(s[s.length-1]+=` `,c++));let i=T(r);if(n.hard&&i>t){let e=t-c,n=1+Math.floor((i-e-1)/t);Math.floor((i-1)/t)<n&&s.push(``),j(s,r,t),c=T(s.at(-1)??``);continue}if(c+i>t&&c&&i){if(n.wordWrap===!1&&c<t){j(s,r,t),c=T(s.at(-1)??``);continue}s.push(``),c=0}if(c+i>t&&n.wordWrap===!1){j(s,r,t),c=T(s.at(-1)??``);continue}s[s.length-1]+=r,c+=i}n.trim!==!1&&(s=s.map(e=>M(e)));let l=s.join(`
|
|
3
|
+
`),u=!1;for(let e=0;e<l.length;e++){let t=l[e];if(r+=t,u)u=!1;else if(u=t>=`\ud800`&&t<=`\udbff`,u)continue;if(t===`\x1B`||t===``){D.lastIndex=e+1;let t=D.exec(l)?.groups;if(t?.code!==void 0){let e=Number.parseFloat(t.code);i=e===39?void 0:e}else t?.uri!==void 0&&(a=t.uri.length===0?void 0:t.uri)}if(l[e+1]===`
|
|
4
|
+
`){a&&(r+=A(``));let e=i?O(i):void 0;i&&e&&(r+=k(e))}else t===`
|
|
5
|
+
`&&(i&&O(i)&&(r+=k(i)),a&&(r+=A(a)))}return r},re=/\r?\n/;function N(e,t,n){return String(e).normalize().split(re).map(e=>ne(e,t,n)).join(`
|
|
6
|
+
`)}var P=p(((e,t)=>{let n={to(e,t){return t?`[${t+1};${e+1}H`:`[${e+1}G`},move(e,t){let n=``;return e<0?n+=`[${-e}D`:e>0&&(n+=`[${e}C`),t<0?n+=`[${-t}A`:t>0&&(n+=`[${t}B`),n},up:(e=1)=>`[${e}A`,down:(e=1)=>`[${e}B`,forward:(e=1)=>`[${e}C`,backward:(e=1)=>`[${e}D`,nextLine:(e=1)=>`\x1B[E`.repeat(e),prevLine:(e=1)=>`\x1B[F`.repeat(e),left:`\x1B[G`,hide:`\x1B[?25l`,show:`\x1B[?25h`,save:`\x1B7`,restore:`\x1B8`};t.exports={cursor:n,scroll:{up:(e=1)=>`\x1B[S`.repeat(e),down:(e=1)=>`\x1B[T`.repeat(e)},erase:{screen:`\x1B[2J`,up:(e=1)=>`\x1B[1J`.repeat(e),down:(e=1)=>`\x1B[J`.repeat(e),line:`\x1B[2K`,lineEnd:`\x1B[K`,lineStart:`\x1B[1K`,lines(e){let t=``;for(let r=0;r<e;r++)t+=this.line+(r<e-1?n.up():``);return e&&(t+=n.left),t}},beep:`\x07`}}))();function F(e,t,n){if(!n.some(e=>!e.disabled))return e;let r=e+t,i=Math.max(n.length-1,0),a=r<0?i:r>i?0:r;return n[a]?.disabled?F(a,t<0?-1:1,n):a}const I={actions:new Set([`up`,`down`,`left`,`right`,`space`,`enter`,`cancel`]),aliases:new Map([[`k`,`up`],[`j`,`down`],[`h`,`left`],[`l`,`right`],[``,`cancel`],[`escape`,`cancel`]]),messages:{cancel:`Canceled`,error:`Something went wrong`},withGuide:!0,accessible:void 0,date:{monthNames:[`January`,`February`,`March`,`April`,`May`,`June`,`July`,`August`,`September`,`October`,`November`,`December`],messages:{required:`Please enter a valid date`,invalidMonth:`There are only 12 months in a year`,invalidDay:(e,t)=>`There are only ${e} days in ${t}`,afterMin:e=>`Date must be on or after ${e.toISOString().slice(0,10)}`,beforeMax:e=>`Date must be on or before ${e.toISOString().slice(0,10)}`}}};function ie(e){if(e!==void 0)return e;if(I.accessible!==void 0)return I.accessible;let t=process.env.ACCESSIBLE;return t!==void 0&&t!==``&&t!==`0`&&t!==`false`}function L(e,t){if(typeof e==`string`)return I.aliases.get(e)===t;for(let n of e)if(n!==void 0&&L(n,t))return!0;return!1}function ae(e,t){if(e===t)return;let n=e.split(`
|
|
7
|
+
`),r=t.split(`
|
|
8
|
+
`),i=Math.max(n.length,r.length),a=[];for(let e=0;e<i;e++)n[e]!==r[e]&&a.push(e);return{lines:a,numLinesBefore:n.length,numLinesAfter:r.length,numLines:i}}const oe=globalThis.process.platform.startsWith(`win`),R=Symbol(`clack:cancel`);function z(e){return e===R}function B(e,t){let n=e;n.isTTY&&n.setRawMode(t)}function se({input:e=c,output:t=l,overwrite:n=!0,hideCursor:r=!0}={}){let i=u.createInterface({input:e,output:t,prompt:``,tabSize:1});u.emitKeypressEvents(e,i),e instanceof f&&e.isTTY&&e.setRawMode(!0);let a=(i,{name:o,sequence:s})=>{if(L([String(i),o,s],`cancel`)){r&&t.write(P.cursor.show),process.exit(0);return}if(!n)return;let c=o===`return`?0:-1,l=o===`return`?-1:0;u.moveCursor(t,c,l,()=>{u.clearLine(t,1,()=>{e.once(`keypress`,a)})})};return r&&t.write(P.cursor.hide),e.once(`keypress`,a),()=>{e.off(`keypress`,a),r&&t.write(P.cursor.show),e instanceof f&&e.isTTY&&!oe&&e.setRawMode(!1),i.terminal=!1,i.close()}}const V=e=>`columns`in e&&typeof e.columns==`number`?e.columns:80,ce=e=>`rows`in e&&typeof e.rows==`number`?e.rows:20;function H(e,t,n,r=n,i=n,a){return N(t,V(e??l)-n.length,{hard:!0,trim:!1}).split(`
|
|
9
|
+
`).map((e,t,o)=>{let s=a?a(e,t):e;return t===0?`${r}${s}`:t===o.length-1?`${i}${s}`:`${n}${s}`}).join(`
|
|
10
|
+
`)}function le(e,t){if(`~standard`in e){let n=e[`~standard`].validate(t);return n instanceof Promise?n.then(e=>e.issues?.at(0)?.message):n.issues?.at(0)?.message}return e(t)}var U=class{input;output;_abortSignal;rl;opts;_render;_track=!1;_prevFrame=``;_subscribers=new Map;_cursor=0;state=`initial`;error=``;value;userInput=``;get accessible(){return ie(this.opts.accessible)}constructor(e,t=!0){let{input:n=c,output:r=l,render:i,signal:a,...o}=e;this.opts=o,this.onKeypress=this.onKeypress.bind(this),this.close=this.close.bind(this),this.render=this.render.bind(this),this._render=i.bind(this),this._track=t,this._abortSignal=a,this.input=n,this.output=r}unsubscribe(){this._subscribers.clear()}setSubscriber(e,t){let n=this._subscribers.get(e)??[];n.push(t),this._subscribers.set(e,n)}on(e,t){this.setSubscriber(e,{cb:t})}once(e,t){this.setSubscriber(e,{cb:t,once:!0})}emit(e,...t){let n=this._subscribers.get(e)??[],r=[];for(let e of n)e.cb(...t),e.once&&r.push(()=>n.splice(n.indexOf(e),1));for(let e of r)e()}prompt(){return new Promise(e=>{if(this._abortSignal){if(this._abortSignal.aborted)return this.state=`cancel`,this.close(),e(R);this._abortSignal.addEventListener(`abort`,()=>{this.state=`cancel`,this.close()},{once:!0})}this.rl=d.createInterface({input:this.input,tabSize:2,prompt:``,escapeCodeTimeout:50,terminal:!0}),this.rl.prompt(),this.opts.initialUserInput!==void 0&&this._setUserInput(this.opts.initialUserInput,!0),this.input.on(`keypress`,this.onKeypress),B(this.input,!0),this.output.on(`resize`,this.render),this.render(),this.once(`submit`,()=>{this.output.write(P.cursor.show),this.output.off(`resize`,this.render),B(this.input,!1),e(this.value)}),this.once(`cancel`,()=>{this.output.write(P.cursor.show),this.output.off(`resize`,this.render),B(this.input,!1),e(R)})})}_isActionKey(e,t){return e===` `}_shouldSubmit(e,t){return!0}_setValue(e){this.value=e,this.emit(`value`,this.value)}_setUserInput(e,t){this.userInput=e??``,this.emit(`userInput`,this.userInput),t&&this._track&&this.rl&&(this.rl.write(this.userInput),this._cursor=this.rl.cursor)}_clearUserInput(){this.rl?.write(null,{ctrl:!0,name:`u`}),this._setUserInput(``)}async onKeypress(e,t){if(this.state!==`validating`){if(this._track&&t.name!==`return`&&(t.name&&this._isActionKey(e,t)&&this.rl?.write(null,{ctrl:!0,name:`h`}),this._cursor=this.rl?.cursor??0,this._setUserInput(this.rl?.line)),this.state===`error`&&(this.state=`active`),t?.name&&(!this._track&&I.aliases.has(t.name)&&this.emit(`cursor`,I.aliases.get(t.name)),I.actions.has(t.name)&&this.emit(`cursor`,t.name)),e&&(e.toLowerCase()===`y`||e.toLowerCase()===`n`)&&this.emit(`confirm`,e.toLowerCase()===`y`),this.emit(`key`,e,t),t?.name===`return`&&this._shouldSubmit(e,t)){if(this.opts.validate){let e=le(this.opts.validate,this.value),t;e instanceof Promise?(this.state=`validating`,this.render(),t=await e):t=e,t&&(this.error=t instanceof Error?t.message:t,this.state=`error`,this.rl?.write(this.userInput))}this.state!==`error`&&(this.state=`submit`)}L([e,t?.name,t?.sequence],`cancel`)&&(this.state=`cancel`),(this.state===`submit`||this.state===`cancel`)&&this.emit(`finalize`),this.render(),(this.state===`submit`||this.state===`cancel`)&&this.close()}}close(){this.input.unpipe(),this.input.removeListener(`keypress`,this.onKeypress),this.output.write(`
|
|
11
|
+
`),B(this.input,!1),this.rl?.close(),this.rl=void 0,this.emit(`${this.state}`,this.value),this.unsubscribe()}restoreCursor(){let e=N(this._prevFrame,process.stdout.columns,{hard:!0,trim:!1}).split(`
|
|
12
|
+
`).length-1;this.output.write(P.cursor.move(-999,e*-1))}render(){let e=N(this._render(this)??``,process.stdout.columns,{hard:!0,trim:!1});if(e!==this._prevFrame){if(this.state===`initial`)this.output.write(P.cursor.hide);else{let t=ae(this._prevFrame,e),n=ce(this.output);if(this.restoreCursor(),t){let r=Math.max(0,t.numLinesAfter-n),i=Math.max(0,t.numLinesBefore-n),a=t.lines.find(e=>e>=r);if(a===void 0){this._prevFrame=e;return}if(t.lines.length===1){this.output.write(P.cursor.move(0,a-i)),this.output.write(P.erase.lines(1));let t=e.split(`
|
|
13
|
+
`);this.output.write(t[a]),this._prevFrame=e,this.output.write(P.cursor.move(0,t.length-a-1));return}if(t.lines.length>1){if(r<i)a=r;else{let e=a-i;e>0&&this.output.write(P.cursor.move(0,e))}this.output.write(P.erase.down());let t=e.split(`
|
|
14
|
+
`).slice(a);this.output.write(t.join(`
|
|
15
|
+
`)),this._prevFrame=e;return}}this.output.write(P.erase.down())}this.output.write(e),this.state===`initial`&&(this.state=`active`),this._prevFrame=e}}},ue=class extends U{options;cursor=0;get _value(){return this.options[this.cursor]?.value}get _enabledOptions(){return this.options.filter(e=>e.disabled!==!0)}toggleAll(){let e=this._enabledOptions,t=this.value!==void 0&&this.value.length===e.length;this.value=t?[]:e.map(e=>e.value)}toggleInvert(){let e=this.value;if(!e)return;let t=this._enabledOptions.filter(t=>!e.includes(t.value));this.value=t.map(e=>e.value)}toggleValue(){this.value===void 0&&(this.value=[]);let e=this.value.includes(this._value);this.value=e?this.value.filter(e=>e!==this._value):[...this.value,this._value]}constructor(e){super(e,!1),this.options=e.options,this.value=[...e.initialValues??[]];let t=Math.max(this.options.findIndex(({value:t})=>t===e.cursorAt),0);this.cursor=this.options[t]?.disabled?F(t,1,this.options):t,this.on(`key`,(e,t)=>{t.name===`a`&&this.toggleAll(),t.name===`i`&&this.toggleInvert()}),this.on(`cursor`,e=>{switch(e){case`left`:case`up`:this.cursor=F(this.cursor,-1,this.options);break;case`down`:case`right`:this.cursor=F(this.cursor,1,this.options);break;case`space`:this.toggleValue()}})}};let de=class extends U{options;cursor=0;get _selectedValue(){return this.options[this.cursor]}changeValue(){let e=this._selectedValue;this.value=e===void 0?void 0:e.value}constructor(e){super(e,!1),this.options=e.options;let t=this.options.findIndex(({value:t})=>t===e.initialValue),n=t===-1?0:t;this.cursor=this.options[n]?.disabled?F(n,1,this.options):n,this.changeValue(),this.on(`cursor`,e=>{switch(e){case`left`:case`up`:this.cursor=F(this.cursor,-1,this.options);break;case`down`:case`right`:this.cursor=F(this.cursor,1,this.options)}this.changeValue()})}};var fe=class extends U{get userInputWithCursor(){if(this.state===`submit`)return this.userInput;let e=this.userInput;if(this.cursor>=e.length)return`${this.userInput}\u2588`;let t=e.slice(0,this.cursor),n=e.slice(this.cursor,this.cursor+1),r=e.slice(this.cursor+1);return`${t}${o(`inverse`,n)}${r}`}get cursor(){return this._cursor}constructor(e){super({...e,initialUserInput:e.initialUserInput??e.initialValue}),this.on(`userInput`,e=>{this._setValue(e)}),this.on(`finalize`,()=>{this.value||=e.defaultValue,this.value===void 0&&(this.value=``)})}};function pe(){return s.platform===`win32`?!!s.env.CI||!!s.env.WT_SESSION||!!s.env.TERMINUS_SUBLIME||s.env.ConEmuTask===`{cmd::Cmder}`||s.env.TERM_PROGRAM===`Terminus-Sublime`||s.env.TERM_PROGRAM===`vscode`||s.env.TERM===`xterm-256color`||s.env.TERM===`alacritty`||s.env.TERMINAL_EMULATOR===`JetBrains-JediTerm`:s.env.TERM!==`linux`}const W=pe(),me=()=>process.env.CI===`true`,G=(e,t)=>W?e:t,he=G(`◆`,`*`),ge=G(`■`,`x`),_e=G(`▲`,`x`),ve=G(`◇`,`o`),ye=G(`┌`,`T`),K=G(`│`,`|`),q=G(`└`,`—`),be=G(`●`,`>`),xe=G(`○`,` `),Se=G(`◻`,`[•]`),Ce=G(`◼`,`[+]`),we=G(`◻`,`[ ]`),J=e=>{switch(e){case`initial`:case`active`:return o(`cyan`,he);case`cancel`:return o(`red`,ge);case`error`:return o(`yellow`,_e);case`submit`:return o(`green`,ve);case`validating`:return o(`dim`,he)}},Te=e=>{switch(e){case`initial`:case`active`:return o(`cyan`,K);case`cancel`:return o(`red`,K);case`error`:return o(`yellow`,K);case`submit`:return o(`green`,K)}};function Ee(e,t){let n=[`${t?`${o(`cyan`,K)} `:``}${e.join(` • `)}`];return t&&n.push(o(`cyan`,q)),n}const De=(e,t,n,r,i,a=!1)=>{let o=t,s=0;if(a)for(let t=r-1;t>=n;t--){let n=e[t];if(n&&(o-=n.length),s++,o<=i)break}else for(let t=n;t<r;t++){let n=e[t];if(n&&(o-=n.length),s++,o<=i)break}return{lineCount:o,removals:s}},Y=({cursor:e,options:t,style:n,output:r=process.stdout,maxItems:i=1/0,columnPadding:a=0,rowPadding:s=4})=>{let c=V(r)-a,l=ce(r),u=o(`dim`,`...`),d=Math.max(l-s,0),f=Math.max(Math.min(i,d),5),p=0;e>=f-3&&(p=Math.max(Math.min(e-f+3,t.length-f),0));let m=f<t.length&&p>0,h=f<t.length&&p+f<t.length,g=Math.min(p+f,t.length),_=[],v=0;m&&v++,h&&v++;let y=p+ +!!m,b=g-+!!h;for(let r=y;r<b;r++){let i=t[r],a=N(i?n(i,r===e):``,c,{hard:!0,trim:!1}).split(`
|
|
16
|
+
`);_.push(a),v+=a.length}if(v>d){let t=0,n=0,r=v,i=e-y,a=d,o=()=>De(_,r,0,i,a),s=()=>De(_,r,i+1,_.length,a,!0);m?({lineCount:r,removals:t}=o(),r>a&&(h||--a,{lineCount:r,removals:n}=s())):(h||--a,{lineCount:r,removals:n}=s(),r>a&&(--a,{lineCount:r,removals:t}=o())),t>0&&(m=!0,_.splice(0,t)),n>0&&(h=!0,_.splice(_.length-n,n))}let x=[];m&&x.push(u);for(let e of _)for(let t of e)x.push(t);return h&&x.push(u),x},Oe=[`${o(`dim`,`↑/↓`)} to navigate`,`${o(`dim`,`Space:`)} select`,`${o(`dim`,`Enter:`)} confirm`],X=(e,t)=>e.split(`
|
|
17
|
+
`).map(e=>t(e)).join(`
|
|
18
|
+
`),ke=e=>{let t=(e,t)=>{let n=e.label??String(e.value);return t===`disabled`?`${o(`gray`,we)} ${X(n,e=>o([`strikethrough`,`gray`],e))}${e.hint?` ${o(`dim`,`(${e.hint??`disabled`})`)}`:``}`:t===`active`?`${o(`cyan`,Se)} ${n}${e.hint?` ${o(`dim`,`(${e.hint})`)}`:``}`:t===`selected`?`${o(`green`,Ce)} ${X(n,e=>o(`dim`,e))}${e.hint?` ${o(`dim`,`(${e.hint})`)}`:``}`:t===`cancelled`?`${X(n,e=>o([`strikethrough`,`dim`],e))}`:t===`active-selected`?`${o(`green`,Ce)} ${n}${e.hint?` ${o(`dim`,`(${e.hint})`)}`:``}`:t===`submitted`?`${X(n,e=>o(`dim`,e))}`:`${o(`dim`,we)} ${X(n,e=>o(`dim`,e))}`},n=e.required??!0,r=e.showInstructions??!0;return new ue({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValues:e.initialValues,required:n,cursorAt:e.cursorAt,validate(e){if(n&&(e===void 0||e.length===0))return`Please select at least one option.
|
|
19
|
+
${o(`reset`,o(`dim`,`Press ${o([`gray`,`bgWhite`,`inverse`],` space `)} to select, ${o(`gray`,o(`bgWhite`,o(`inverse`,` enter `)))} to submit`))}`},render(){let n=e.withGuide??I.withGuide,i=H(e.output,e.message,n?`${Te(this.state)} `:``,`${J(this.state)} `),a=`${n?`${o(`gray`,K)}
|
|
20
|
+
`:``}${i}
|
|
21
|
+
`,s=this.value??[],c=(e,n)=>{if(e.disabled)return t(e,`disabled`);let r=s.includes(e.value);return n&&r?t(e,`active-selected`):r?t(e,`selected`):t(e,n?`active`:`inactive`)};switch(this.state){case`submit`:{let r=this.options.filter(({value:e})=>s.includes(e)).map(e=>t(e,`submitted`)).join(o(`dim`,`, `))||o(`dim`,`none`);return`${a}${H(e.output,r,n?`${o(`gray`,K)} `:``)}`}case`cancel`:{let r=this.options.filter(({value:e})=>s.includes(e)).map(e=>t(e,`cancelled`)).join(o(`dim`,`, `));return r.trim()===``?`${a}${o(`gray`,K)}`:`${a}${H(e.output,r,n?`${o(`gray`,K)} `:``)}${n?`
|
|
22
|
+
${o(`gray`,K)}`:``}`}case`error`:{let t=n?`${o(`yellow`,K)} `:``,r=this.error.split(`
|
|
23
|
+
`).map((e,t)=>t===0?`${n?`${o(`yellow`,q)} `:``}${o(`yellow`,e)}`:` ${e}`).join(`
|
|
24
|
+
`),i=a.split(`
|
|
25
|
+
`).length,s=r.split(`
|
|
26
|
+
`).length+1;return`${a}${t}${Y({output:e.output,options:this.options,cursor:this.cursor,maxItems:e.maxItems,columnPadding:t.length,rowPadding:i+s,style:c}).join(`
|
|
27
|
+
${t}`)}
|
|
28
|
+
${r}
|
|
29
|
+
`}default:{let t=n?`${o(`cyan`,K)} `:``,i=a.split(`
|
|
30
|
+
`).length,s=r?Ee(Oe,n):n?[o(`cyan`,q)]:[],l=s.join(`
|
|
31
|
+
`),u=s.length+1;return`${a}${t}${Y({output:e.output,options:this.options,cursor:this.cursor,maxItems:e.maxItems,columnPadding:t.length,rowPadding:i+u,style:c}).join(`
|
|
32
|
+
${t}`)}
|
|
33
|
+
${l}
|
|
34
|
+
`}}}}).prompt()},Z=(e=``,t)=>{let n=t?.output??process.stdout,r=t?.withGuide??I.withGuide?`${o(`gray`,q)} `:``;n.write(`${r}${o(`red`,e)}
|
|
35
|
+
|
|
36
|
+
`)},Ae=(e=``,t)=>{let n=t?.output??process.stdout,r=t?.withGuide??I.withGuide?`${o(`gray`,ye)} `:``;n.write(`${r}${e}
|
|
37
|
+
`)},je=(e=``,t)=>{let n=t?.output??process.stdout,r=t?.withGuide??I.withGuide?`${o(`gray`,K)}
|
|
38
|
+
${o(`gray`,q)} `:``;n.write(`${r}${e}
|
|
39
|
+
|
|
40
|
+
`)},Me=e=>o(`magenta`,e),Ne=({indicator:e=`dots`,onCancel:t,output:n=process.stdout,cancelMessage:r,errorMessage:i,frames:a=W?[`◒`,`◐`,`◓`,`◑`]:[`•`,`o`,`O`,`0`],delay:s=W?80:120,signal:c,...l}={})=>{let u=me(),d,f,p=!1,m=!1,h=``,g,_=performance.now(),v=V(n),y=l?.styleFrame??Me,b=e=>{let n=e>1?i??I.messages.error:r??I.messages.cancel;m=e===1,p&&(O(n,e),m&&typeof t==`function`&&t())},x=()=>b(2),S=()=>b(1),ee=()=>{process.on(`uncaughtExceptionMonitor`,x),process.on(`unhandledRejection`,x),process.on(`SIGINT`,S),process.on(`SIGTERM`,S),process.on(`exit`,b),c&&c.addEventListener(`abort`,S)},te=()=>{process.removeListener(`uncaughtExceptionMonitor`,x),process.removeListener(`unhandledRejection`,x),process.removeListener(`SIGINT`,S),process.removeListener(`SIGTERM`,S),process.removeListener(`exit`,b),c&&c.removeEventListener(`abort`,S)},C=()=>{if(g===void 0)return;u&&n.write(`
|
|
41
|
+
`);let e=N(g,v,{hard:!0,trim:!1}).split(`
|
|
42
|
+
`);e.length>1&&n.write(P.cursor.up(e.length-1)),n.write(P.cursor.to(0)),n.write(P.erase.down())},w=e=>e.replace(/\.+$/,``),T=e=>{let t=(performance.now()-e)/1e3,n=Math.floor(t/60),r=Math.floor(t%60);return n>0?`[${n}m ${r}s]`:`[${r}s]`},E=l.withGuide??I.withGuide,D=(t=``)=>{p=!0,d=se({output:n}),h=w(t),_=performance.now(),E&&n.write(`${o(`gray`,K)}
|
|
43
|
+
`);let r=0,i=0;ee(),f=setInterval(()=>{if(u&&h===g)return;C(),g=h;let t=y(a[r]),o;if(u)o=`${t} ${h}...`;else if(e===`timer`)o=`${t} ${h} ${T(_)}`;else{let e=`.`.repeat(Math.floor(i)).slice(0,3);o=`${t} ${h}${e}`}let s=N(o,v,{hard:!0,trim:!1});n.write(s),r=r+1<a.length?r+1:0,i=i<4?i+.125:0},s)},O=(t=``,r=0,i=!1)=>{if(!p)return;p=!1,clearInterval(f),C();let a=r===0?o(`green`,ve):r===1?o(`red`,ge):o(`red`,_e);h=t??h,i||(e===`timer`?n.write(`${a} ${h} ${T(_)}
|
|
44
|
+
`):n.write(`${a} ${h}
|
|
45
|
+
`)),te(),d()};return{start:D,stop:(e=``)=>O(e,0),message:(e=``)=>{h=w(e??h)},cancel:(e=``)=>O(e,1),error:(e=``)=>O(e,2),clear:()=>O(``,0,!0),get isCancelled(){return m}}},Pe=[`${o(`dim`,`↑/↓`)} to navigate`,`${o(`dim`,`Enter:`)} confirm`],Q=(e,t)=>e.includes(`
|
|
46
|
+
`)?e.split(`
|
|
47
|
+
`).map(e=>t(e)).join(`
|
|
48
|
+
`):t(e),Fe=e=>{let t=(e,t)=>{if(e===void 0)return``;let n=e.label??String(e.value);switch(t){case`disabled`:return`${o(`gray`,xe)} ${Q(n,e=>o(`gray`,e))}${e.hint?` ${o(`dim`,`(${e.hint??`disabled`})`)}`:``}`;case`selected`:return`${Q(n,e=>o(`dim`,e))}`;case`active`:return`${o(`green`,be)} ${n}${e.hint?` ${o(`dim`,`(${e.hint})`)}`:``}`;case`cancelled`:return`${Q(n,e=>o([`strikethrough`,`dim`],e))}`;default:return`${o(`dim`,xe)} ${Q(n,e=>o(`dim`,e))}`}},n=e.showInstructions??!0;return new de({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValue:e.initialValue,render(){let r=e.withGuide??I.withGuide,i=`${J(this.state)} `,a=`${Te(this.state)} `,s=H(e.output,e.message,a,i),c=`${r?`${o(`gray`,K)}
|
|
49
|
+
`:``}${s}
|
|
50
|
+
`;switch(this.state){case`submit`:{let n=r?`${o(`gray`,K)} `:``;return`${c}${H(e.output,t(this.options[this.cursor],`selected`),n)}`}case`cancel`:{let n=r?`${o(`gray`,K)} `:``;return`${c}${H(e.output,t(this.options[this.cursor],`cancelled`),n)}${r?`
|
|
51
|
+
${o(`gray`,K)}`:``}`}default:{let i=r?`${o(`cyan`,K)} `:``,a=c.split(`
|
|
52
|
+
`).length,s=n?Ee(Pe,r):r?[o(`cyan`,q)]:[],l=s.join(`
|
|
53
|
+
`),u=s.length+1;return`${c}${i}${Y({output:e.output,cursor:this.cursor,options:this.options,maxItems:e.maxItems,columnPadding:i.length,rowPadding:a+u,style:(e,n)=>t(e,e.disabled?`disabled`:n?`active`:`inactive`)}).join(`
|
|
54
|
+
${i}`)}
|
|
55
|
+
${l}
|
|
56
|
+
`}}}}).prompt()};`${o(`gray`,K)}`;const $=e=>new fe({validate:e.validate,placeholder:e.placeholder,defaultValue:e.defaultValue,initialValue:e.initialValue,output:e.output,signal:e.signal,input:e.input,render(){let t=e?.withGuide??I.withGuide,n=`${`${t?`${o(`gray`,K)}
|
|
57
|
+
`:``}${J(this.state)} `}${e.message}
|
|
58
|
+
`,r=e.placeholder&&e.placeholder.length>0?o(`inverse`,e.placeholder[0])+o(`dim`,e.placeholder.slice(1)):o([`inverse`,`hidden`],`_`),i=this.userInput?this.userInputWithCursor:r,a=this.value??``;switch(this.state){case`validating`:{let e=t?`${o(`cyan`,K)} `:``,r=t?o(`cyan`,q):``;return`${n}${e}${o(`dim`,i)}
|
|
59
|
+
${r} ${o(`dim`,`Validating...`)}
|
|
60
|
+
`}case`error`:{let e=this.error?` ${o(`yellow`,this.error)}`:``,r=t?`${o(`yellow`,K)} `:``,a=t?o(`yellow`,q):``;return`${n.trim()}
|
|
61
|
+
${r}${i}
|
|
62
|
+
${a}${e}
|
|
63
|
+
`}case`submit`:{let e=a?`${t?` `:``}${o(`dim`,a)}`:``;return`${n}${t?o(`gray`,K):``}${e}`}case`cancel`:{let e=a?` ${o([`strikethrough`,`dim`],a)}`:``,r=t?o(`gray`,K):``;return`${n}${r}${e}${a.trim()?`
|
|
64
|
+
${r}`:``}`}default:return`${n}${t?`${o(`cyan`,K)} `:``}${i}
|
|
65
|
+
${t?o(`cyan`,q):``}
|
|
66
|
+
`}}}).prompt();function Ie(e){let t=e.find(e=>!e.startsWith(`--`));if(t===void 0||t.startsWith(`-`))throw Error(`Usage: bunx @jchat/create-addon <addon-id> [--target=<directory>] [--image=<image>] [--workspace] [--template=ui|fullstack] [--mount-points=p1,p2] [--name=<displayName>]`);let n=e.find(e=>e.startsWith(`--target=`)),r=e.find(e=>e.startsWith(`--image=`)),i=e.find(e=>e.startsWith(`--template=`)),o=e.find(e=>e.startsWith(`--mount-points=`)||e.startsWith(`--mountPoints=`)),s=e.find(e=>e.startsWith(`--name=`)),c=e.includes(`--workspace`),l=c?a.join(`addons`,t):t,u=i?i.split(`=`)[1]:void 0,d=o?o.split(`=`)[1]?.split(`,`).filter(Boolean):void 0,f=s?s.slice(7):void 0;return{addonId:t,targetDir:n?.slice(9)||l,sandboxImage:r?.slice(8)||void 0,workspace:c,...u===void 0?{}:{template:u},...d===void 0?{}:{mountPoints:d},...f===void 0?{}:{displayName:f}}}function Le(){try{let e=process.cwd();for(let t=0;t<6;t++){let t=a.join(e,`package.json`);if(i.existsSync(t)){let e=i.readFileSync(t,`utf8`),n=JSON.parse(e);if(n.name===`jchat`&&Array.isArray(n.workspaces))return!0}let n=a.dirname(e);if(n===e)break;e=n}}catch{}return!1}async function Re(t){Ae(`@jchat/create-addon`);let r=t?.trim();if(!r||!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(r)){let e=await $({message:`What is your addon ID?`,placeholder:`my-custom-addon`,initialValue:t,validate(e){if(!e||e.trim().length===0)return`Addon ID is required`;if(!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(e.trim()))return`Addon ID must contain lowercase letters, numbers, and hyphens (e.g. my-custom-addon)`}});z(e)&&(Z(`Scaffolding cancelled.`),process.exit(0)),r=String(e).trim()}let i=await $({message:`What is the display name?`,initialValue:n(r),validate(e){if(!e||e.trim().length===0)return`Display name is required`}});z(i)&&(Z(`Scaffolding cancelled.`),process.exit(0));let o=await Fe({message:`Choose an addon template:`,options:[{value:`fullstack`,label:`Full-Stack (UI + Backend API + Database)`,hint:`Recommended - includes Elysia routes, Kysely DB, migrations, permissions`},{value:`ui`,label:`Frontend Only (UI Component + Locales)`,hint:`Lightweight client-only extension`}],initialValue:`fullstack`});z(o)&&(Z(`Scaffolding cancelled.`),process.exit(0));let s=await ke({message:`Where should the addon be mounted?`,options:[{value:e.CHAT_TOOLBAR,label:`Chat Toolbar`,hint:`Top header/toolbar in chat`},{value:e.CHAT_PLUS_MENU,label:`Plus Menu (+)`,hint:`Message composer action menu`},{value:e.CHAT_SIDEBAR,label:`Chat Sidebar`,hint:`Right-hand sidebar widget`},{value:e.ADDON_SETTINGS,label:`Addon Settings`,hint:`Site admin dashboard panel`},{value:e.OWNER_SETTINGS,label:`Owner Settings`,hint:`Owner dashboard panel`},{value:e.EMOJI_PICKER_GIF_TAB,label:`GIF Tab`,hint:`Emoji/GIF picker panel tab`},{value:e.EMOJI_PICKER_STICKER_TAB,label:`Sticker Tab`,hint:`Emoji/Sticker picker panel tab`}],initialValues:[e.CHAT_TOOLBAR,e.CHAT_PLUS_MENU],required:!0});z(s)&&(Z(`Scaffolding cancelled.`),process.exit(0));let c=Le(),l=await Fe({message:`Target environment:`,options:c?[{value:`workspace`,label:`JChat Monorepo Workspace (addons/<id>)`,hint:`Detected JChat root`},{value:`standalone`,label:`Standalone DevKit (Docker Sandbox)`,hint:`Independent repository`}]:[{value:`standalone`,label:`Standalone DevKit (Docker Sandbox)`,hint:`Independent repository`},{value:`workspace`,label:`Monorepo Workspace (addons/<id>)`,hint:`Inside existing monorepo`}],initialValue:c?`workspace`:`standalone`});z(l)&&(Z(`Scaffolding cancelled.`),process.exit(0));let u=l===`workspace`,d=u?a.join(`addons`,r):r,f=await $({message:`Target directory:`,initialValue:d,validate(e){if(!e||e.trim().length===0)return`Target directory is required`}});return z(f)&&(Z(`Scaffolding cancelled.`),process.exit(0)),{addonId:r,targetDir:String(f).trim(),displayName:String(i).trim(),template:o,mountPoints:s,workspace:u}}async function ze(e){let n=await Bun.file(new URL(`../package.json`,import.meta.url)).json();if(e.includes(`--help`)||e.includes(`-h`))return console.log(`Usage: bunx @jchat/create-addon [addon-id] [--target=<directory>] [--image=<image>] [--workspace] [--template=ui|fullstack] [--mount-points=p1,p2] [--name=<displayName>]`),``;let i=!e.some(e=>e.startsWith(`-`))&&!(!process.stdin.isTTY||process.env.CI),o=e.find(e=>!e.startsWith(`-`)),s=i?await Re(o):Ie(e),c=i?Ne():void 0;c?.start(`Scaffolding ${s.displayName??s.addonId}...`);let l=``;return s.workspace?(await t({addonId:s.addonId,targetDir:s.targetDir,isMonorepoWorkspace:!0,jchatVersion:n.version,addonSdkVersion:n.version,template:s.template,mountPoints:s.mountPoints,displayName:s.displayName}),l=a.resolve(s.targetDir),c?.stop(`Addon created successfully at ${l}`),i&&je(`Next steps:
|
|
67
|
+
cd ${s.targetDir}
|
|
68
|
+
bun run dev`)):(await r({...s,jchatVersion:n.version,addonSdkVersion:n.version,sandboxImage:s.sandboxImage??`${n.jchat?.sandboxImage??`ghcr.io/mjjabarullah/jchat-sandbox`}:${n.version.replaceAll(`+`,`-`)}`,template:s.template,mountPoints:s.mountPoints,displayName:s.displayName}),l=a.resolve(s.targetDir),c?.stop(`Standalone DevKit created successfully at ${l}`),i&&je(`Next steps:
|
|
69
|
+
cd ${s.targetDir}
|
|
70
|
+
cp .env.example .env
|
|
71
|
+
bun install
|
|
72
|
+
bun sandbox`)),i||console.log(`Created addon at ${l}`),l}if(import.meta.main)try{await ze(process.argv.slice(2))}catch(e){console.error(e),process.exit(1)}export{Le as isInsideJChatMonorepo,Ie as parseCreatorArguments,Re as promptInteractiveOptions,ze as runCreator};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region ../addon-sdk/dist/contracts.d.mts
|
|
2
|
+
/** Valid locations where an addon can be rendered within the Jchat layout. */
|
|
3
|
+
declare const MOUNT_POINTS: {
|
|
4
|
+
/** Top of the main chat column */
|
|
5
|
+
readonly CHAT_HEADER: "chat-header";
|
|
6
|
+
/** Below the main chat header, above messages */
|
|
7
|
+
readonly CHAT_TOOLBAR: "chat-toolbar";
|
|
8
|
+
/** The persistent right-side sidebar */
|
|
9
|
+
readonly CHAT_SIDEBAR: "chat-sidebar";
|
|
10
|
+
/** Top of the left sidebar (room list) */
|
|
11
|
+
readonly SIDEBAR_TOP: "sidebar-top";
|
|
12
|
+
/** Bottom of the left sidebar */
|
|
13
|
+
readonly SIDEBAR_BOTTOM: "sidebar-bottom";
|
|
14
|
+
/** Action list shown when clicking a user's avatar */
|
|
15
|
+
readonly USER_POPOVER_ACTIONS: "user-popover-actions";
|
|
16
|
+
/** Menu shown when right-clicking a message */
|
|
17
|
+
readonly MESSAGE_CONTEXT_MENU: "message-context-menu";
|
|
18
|
+
/** Floating overlay rendered on top of the message stream */
|
|
19
|
+
readonly MESSAGE_FEED_OVERLAY: "message-feed-overlay";
|
|
20
|
+
/** Main admin dashboard area */
|
|
21
|
+
readonly ADMIN_PANEL: "admin-panel";
|
|
22
|
+
/** User or site settings dashboard area */
|
|
23
|
+
readonly SETTINGS_PANEL: "settings-panel";
|
|
24
|
+
/** The 'plus' (+) menu in the message composer */
|
|
25
|
+
readonly CHAT_PLUS_MENU: "chat-plus-menu";
|
|
26
|
+
/** Custom tab in the emoji/gif selection panel */
|
|
27
|
+
readonly EMOJI_PICKER_GIF_TAB: "emoji-picker-gif-tab";
|
|
28
|
+
/** Custom tab in the emoji/sticker selection panel */
|
|
29
|
+
readonly EMOJI_PICKER_STICKER_TAB: "emoji-picker-sticker-tab";
|
|
30
|
+
/** Full-screen or modal dialog specifically for TubeScan */
|
|
31
|
+
readonly TUBESCAN_DIALOG: "tubescan-dialog";
|
|
32
|
+
/** Dedicated settings panel rendered in the Owner Dashboard */
|
|
33
|
+
readonly OWNER_SETTINGS: "owner-settings";
|
|
34
|
+
/** Dedicated settings panel rendered in the Site Admin Dashboard */
|
|
35
|
+
readonly ADDON_SETTINGS: "addon-settings";
|
|
36
|
+
};
|
|
37
|
+
/** Union type of all valid addon mount points. */
|
|
38
|
+
type MountPoint = (typeof MOUNT_POINTS)[keyof typeof MOUNT_POINTS];
|
|
39
|
+
//#endregion
|
|
40
|
+
export { MountPoint as n, MOUNT_POINTS as t };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { n as MountPoint } from "./contracts.mjs";
|
|
2
|
+
//#region src/generator.d.ts
|
|
3
|
+
type AddonTemplateType = "ui" | "fullstack";
|
|
4
|
+
interface StandaloneDevkitOptions {
|
|
5
|
+
addonId: string;
|
|
6
|
+
targetDir: string;
|
|
7
|
+
jchatVersion: string;
|
|
8
|
+
addonSdkVersion: string;
|
|
9
|
+
sandboxImage?: string;
|
|
10
|
+
template?: AddonTemplateType;
|
|
11
|
+
mountPoints?: MountPoint[];
|
|
12
|
+
displayName?: string;
|
|
13
|
+
}
|
|
14
|
+
interface AddonWorkspaceOptions {
|
|
15
|
+
addonId: string;
|
|
16
|
+
targetDir: string;
|
|
17
|
+
isMonorepoWorkspace?: boolean;
|
|
18
|
+
jchatVersion: string;
|
|
19
|
+
addonSdkVersion: string;
|
|
20
|
+
template?: AddonTemplateType;
|
|
21
|
+
mountPoints?: MountPoint[];
|
|
22
|
+
displayName?: string;
|
|
23
|
+
}
|
|
24
|
+
declare const HOST_LOCALES: readonly ["ar", "bg", "de", "el", "en", "es", "fr", "he", "hi", "hr", "kn", "ml", "nl", "pt", "ro", "ru", "ta", "te", "tr", "ur"];
|
|
25
|
+
declare function createAddonFiles(options: {
|
|
26
|
+
addonId: string;
|
|
27
|
+
isMonorepoWorkspace?: boolean;
|
|
28
|
+
jchatVersion: string;
|
|
29
|
+
addonSdkVersion: string;
|
|
30
|
+
template?: AddonTemplateType;
|
|
31
|
+
mountPoints?: MountPoint[];
|
|
32
|
+
displayName?: string;
|
|
33
|
+
}): Readonly<Record<string, string>>;
|
|
34
|
+
declare function writeAddonWorkspace(options: AddonWorkspaceOptions): Promise<void>;
|
|
35
|
+
declare function createStandaloneDevkitFiles(options: StandaloneDevkitOptions): Readonly<Record<string, string>>;
|
|
36
|
+
declare function writeStandaloneDevkit(options: StandaloneDevkitOptions): Promise<void>;
|
|
37
|
+
declare function toDisplayName(addonId: string): string;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { createAddonFiles as a, writeAddonWorkspace as c, StandaloneDevkitOptions as i, writeStandaloneDevkit as l, AddonWorkspaceOptions as n, createStandaloneDevkitFiles as o, HOST_LOCALES as r, toDisplayName as s, AddonTemplateType as t };
|
package/dist/generator.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
`),"README.md":
|
|
3
|
-
`)}function
|
|
1
|
+
import{t as e}from"./contracts.d.mts";import*as t from"node:fs";import*as n from"node:path";var[r]=[8,()=>[],[``]];const i=[`ar`,`bg`,`de`,`el`,`en`,`es`,`fr`,`he`,`hi`,`hr`,`kn`,`ml`,`nl`,`pt`,`ro`,`ru`,`ta`,`te`,`tr`,`ur`],a=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,o={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 plugin`,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:`Ugrađeno 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:`Gemonteerd 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 s(t){if(!a.test(t.addonId))throw Error(`Addon id must contain lowercase letters, numbers, and hyphens`);let n=(t.template??`ui`)===`fullstack`,s=t.displayName?.trim()||A(t.addonId),c=t.mountPoints&&t.mountPoints.length>0?t.mountPoints:[e.CHAT_TOOLBAR,e.CHAT_PLUS_MENU],l={id:t.addonId,name:s,version:`1.0.0`,description:`${s} addon for JChat`,author:`Developer`,compatibility:{jchat:r(t.jchatVersion),addonSdk:r(t.addonSdkVersion)},locales:[...i],defaultLocale:`en`,frontend:{bundle:`./ui.js`,mountPoints:c},...n?{server:{bundle:`./api.ts`,migrations:[`migrations/001_initial.sql`]},permissions:[`addon.${t.addonId}.use`,`addon.${t.addonId}.manage`],permissionDefinitions:[{name:`addon.${t.addonId}.use`,label:`Use ${s}`,description:`Access and use ${s} in chat`},{name:`addon.${t.addonId}.manage`,label:`Manage ${s}`,description:`Configure and moderate ${s} settings`}]}:{permissions:[]}},u=t.isMonorepoWorkspace?m(t.addonId,s):_(t.addonId,t.addonSdkVersion),d=t.isMonorepoWorkspace?h():v(),f={"package.json":`${JSON.stringify(u,null,2)}\n`,"tsconfig.json":`${JSON.stringify(d,null,2)}\n`,"src/manifest.ts":y(l),"src/tailwind.css":S(),"src/locales/index.ts":b(),"src/locales.test.ts":x(),"src/ui.tsx":E(s,c,n),".agents/skills/jchat-addon-development/SKILL.md":O()};for(let e of i){let t=o[e];f[`src/locales/${e}.json`]=`${JSON.stringify(t(s),null,2)}\n`}return n&&(f[`src/db.ts`]=C(),f[`src/api.ts`]=T(t.addonId),f[`migrations/001_initial.sql`]=w(t.addonId)),Object.freeze(f)}async function c(e){let r=n.resolve(e.targetDir);if(t.existsSync(r))throw Error(`Addon target already exists: ${r}`);let i=Object.entries(s(e));for(let[e]of i){let i=n.join(r,...e.split(`/`));t.mkdirSync(n.dirname(i),{recursive:!0})}await Promise.all(i.map(([e,t])=>{let i=n.join(r,...e.split(`/`));return Bun.write(i,t)}))}function l(e){if(!a.test(e.addonId))throw Error(`Addon id must contain lowercase letters, numbers, and hyphens`);let t=e.sandboxImage??`ghcr.io/mjjabarullah/jchat-sandbox:${e.jchatVersion.replace(/\+/g,`-`)}`,n=s({addonId:e.addonId,isMonorepoWorkspace:!1,jchatVersion:e.jchatVersion,addonSdkVersion:e.addonSdkVersion,template:e.template,mountPoints:e.mountPoints,displayName:e.displayName}),r={".env.example":d(e.addonId,t),".gitignore":[`node_modules/`,`addon/node_modules/`,`addon/build/`,`.env`,``].join(`
|
|
2
|
+
`),"README.md":k(e.addonId),"docker-compose.yml":f(),"package.json":`${JSON.stringify(p(e.addonId),null,2)}\n`,"scripts/devkit.ts":D(),".agents/skills/jchat-addon-development/SKILL.md":O()};for(let[e,t]of Object.entries(n))r[`addon/${e}`]=t;return Object.freeze(r)}async function u(e){let r=n.resolve(e.targetDir);if(t.existsSync(r))throw Error(`Development kit target already exists: ${r}`);let i=Object.entries(l(e));for(let[e]of i){let i=n.join(r,...e.split(`/`));t.mkdirSync(n.dirname(i),{recursive:!0})}await Promise.all(i.map(([e,t])=>{let i=n.join(r,...e.split(`/`));return Bun.write(i,t)}))}function d(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(`
|
|
3
|
+
`)}function f(){return`services:
|
|
4
4
|
sandbox:
|
|
5
5
|
image: \${JCHAT_SANDBOX_IMAGE:?Copy .env.example to .env}
|
|
6
6
|
restart: unless-stopped
|
|
@@ -73,46 +73,64 @@ volumes:
|
|
|
73
73
|
sandbox_postgres:
|
|
74
74
|
sandbox_redis:
|
|
75
75
|
sandbox_uploads:
|
|
76
|
-
`}function
|
|
76
|
+
`}function p(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 m(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/mjjabarullah/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 h(){return{extends:`@jchat/addon-sdk/tsconfig.json`,include:[`src/**/*`]}}function g(){try{let e=import.meta.dirname;for(let r=0;r<6;r++){let r=n.join(e,`package.json`);if(t.existsSync(r)){let e=t.readFileSync(r,`utf8`),n=JSON.parse(e);if(n.catalog&&typeof n.catalog==`object`)return n.catalog}let i=n.dirname(e);if(i===e)break;e=i}}catch{}return{}}function _(e,t){let n=g();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 v(){return{extends:`@jchat/addon-sdk/tsconfig.json`,include:[`src/**/*.ts`,`src/**/*.tsx`]}}function y(e){return`import type { AddonManifest } from "@jchat/addon-sdk"
|
|
77
77
|
|
|
78
78
|
const manifest: AddonManifest = ${JSON.stringify(e,null,2)}
|
|
79
79
|
|
|
80
80
|
export default manifest
|
|
81
|
-
`}function
|
|
81
|
+
`}function b(){return`import ar from "./ar.json"
|
|
82
|
+
import bg from "./bg.json"
|
|
82
83
|
import de from "./de.json"
|
|
84
|
+
import el from "./el.json"
|
|
83
85
|
import en from "./en.json"
|
|
84
86
|
import es from "./es.json"
|
|
85
87
|
import fr from "./fr.json"
|
|
88
|
+
import he from "./he.json"
|
|
86
89
|
import hi from "./hi.json"
|
|
90
|
+
import hr from "./hr.json"
|
|
87
91
|
import kn from "./kn.json"
|
|
88
92
|
import ml from "./ml.json"
|
|
93
|
+
import nl from "./nl.json"
|
|
94
|
+
import pt from "./pt.json"
|
|
95
|
+
import ro from "./ro.json"
|
|
96
|
+
import ru from "./ru.json"
|
|
89
97
|
import ta from "./ta.json"
|
|
90
98
|
import te from "./te.json"
|
|
99
|
+
import tr from "./tr.json"
|
|
91
100
|
import ur from "./ur.json"
|
|
92
101
|
|
|
93
102
|
export type TranslationKey = keyof typeof en
|
|
94
103
|
|
|
95
104
|
export const locales = {
|
|
96
105
|
ar,
|
|
106
|
+
bg,
|
|
97
107
|
de,
|
|
108
|
+
el,
|
|
98
109
|
en,
|
|
99
110
|
es,
|
|
100
111
|
fr,
|
|
112
|
+
he,
|
|
101
113
|
hi,
|
|
114
|
+
hr,
|
|
102
115
|
kn,
|
|
103
116
|
ml,
|
|
117
|
+
nl,
|
|
118
|
+
pt,
|
|
119
|
+
ro,
|
|
120
|
+
ru,
|
|
104
121
|
ta,
|
|
105
122
|
te,
|
|
123
|
+
tr,
|
|
106
124
|
ur
|
|
107
125
|
} as const
|
|
108
126
|
|
|
109
127
|
export default locales
|
|
110
|
-
`}function
|
|
111
|
-
import { validateAddonLocales } from "@jchat/addon-sdk"
|
|
128
|
+
`}function x(){return`import { describe, expect, test } from "bun:test"
|
|
129
|
+
import { validateAddonLocales } from "@jchat/addon-sdk/i18n-validator"
|
|
112
130
|
import * as path from "node:path"
|
|
113
131
|
|
|
114
132
|
describe("addon locales", () => {
|
|
115
|
-
test("maintains key parity across all
|
|
133
|
+
test("maintains key parity across all 20 host languages", () => {
|
|
116
134
|
const localesDir = path.join(import.meta.dir, "locales")
|
|
117
135
|
const result = validateAddonLocales(localesDir)
|
|
118
136
|
if (!result.valid) {
|
|
@@ -121,16 +139,242 @@ describe("addon locales", () => {
|
|
|
121
139
|
expect(result.valid).toBe(true)
|
|
122
140
|
})
|
|
123
141
|
})
|
|
124
|
-
`}function
|
|
125
|
-
|
|
142
|
+
`}function S(){return`@import "@jchat/addon-sdk/ui/tailwind.css";`}function C(){return`import type { ColumnType } from "@jchat/addon-sdk"
|
|
143
|
+
|
|
144
|
+
export interface AddonItemTable {
|
|
145
|
+
id: string
|
|
146
|
+
siteId: string
|
|
147
|
+
roomId: string
|
|
148
|
+
userId: string
|
|
149
|
+
title: string
|
|
150
|
+
createdAt: ColumnType<Date, Date | string, Date | string>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface AddonDatabase {
|
|
154
|
+
addon_items: AddonItemTable
|
|
155
|
+
}
|
|
156
|
+
`}function w(e){let t=`${e.replace(/-/g,`_`)}_items`;return`CREATE TABLE IF NOT EXISTS "${t}" (
|
|
157
|
+
"id" VARCHAR(36) PRIMARY KEY,
|
|
158
|
+
"siteId" VARCHAR(36) NOT NULL,
|
|
159
|
+
"roomId" VARCHAR(36) NOT NULL,
|
|
160
|
+
"userId" VARCHAR(36) NOT NULL,
|
|
161
|
+
"title" VARCHAR(255) NOT NULL,
|
|
162
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE INDEX IF NOT EXISTS "${`idx_${t}_site_room`}"
|
|
166
|
+
ON "${t}" ("siteId", "roomId", "createdAt" DESC);
|
|
167
|
+
`}function T(e){let t=`${e.replace(/-/g,`_`)}_items`;return`import type { AddonApp, AddonServerContext } from "@jchat/addon-sdk"
|
|
168
|
+
import type { AddonDatabase } from "./db"
|
|
169
|
+
|
|
170
|
+
export default (
|
|
171
|
+
{
|
|
172
|
+
db,
|
|
173
|
+
t,
|
|
174
|
+
cache,
|
|
175
|
+
emit,
|
|
176
|
+
getUser,
|
|
177
|
+
hasPermission
|
|
178
|
+
}: AddonServerContext<AddonDatabase>,
|
|
179
|
+
app: AddonApp
|
|
180
|
+
) => {
|
|
181
|
+
return app
|
|
182
|
+
.get(
|
|
183
|
+
"/items",
|
|
184
|
+
async ({ query, user }) => {
|
|
185
|
+
const siteId = user?.siteId ?? ""
|
|
186
|
+
const roomId = query.roomId ?? ""
|
|
187
|
+
const items = await (db as any)
|
|
188
|
+
.selectFrom("${t}")
|
|
189
|
+
.where("siteId", "=", siteId)
|
|
190
|
+
.where("roomId", "=", roomId)
|
|
191
|
+
.orderBy("createdAt", "desc")
|
|
192
|
+
.limit(20)
|
|
193
|
+
.selectAll()
|
|
194
|
+
.execute()
|
|
195
|
+
|
|
196
|
+
return { items }
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
query: t.Object({
|
|
200
|
+
roomId: t.Optional(t.String())
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
.post(
|
|
205
|
+
"/items",
|
|
206
|
+
async ({ body, user, set }) => {
|
|
207
|
+
if (!user) {
|
|
208
|
+
set.status = 401
|
|
209
|
+
return { error: "Unauthorized" }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const id = Bun.randomUUIDv7()
|
|
213
|
+
const siteId = user.siteId
|
|
214
|
+
const roomId = body.roomId
|
|
215
|
+
const title = body.title
|
|
216
|
+
|
|
217
|
+
await (db as any)
|
|
218
|
+
.insertInto("${t}")
|
|
219
|
+
.values({
|
|
220
|
+
id,
|
|
221
|
+
siteId,
|
|
222
|
+
roomId,
|
|
223
|
+
userId: user.id,
|
|
224
|
+
title,
|
|
225
|
+
createdAt: new Date()
|
|
226
|
+
})
|
|
227
|
+
.execute()
|
|
228
|
+
|
|
229
|
+
await emit("addon:${e}:item-created", { id, title, userId: user.id })
|
|
230
|
+
return { id, title }
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
body: t.Object({
|
|
234
|
+
roomId: t.String(),
|
|
235
|
+
title: t.String({ minLength: 1, maxLength: 255 })
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
`}function E(e,t,n){return n?`import type { AddonProps } from "@jchat/addon-sdk"
|
|
241
|
+
import { useAddonMutation, useAddonQuery, useAddonSDK, useTranslation } from "@jchat/addon-sdk"
|
|
242
|
+
import Icons from "@jchat/addon-sdk/icon"
|
|
243
|
+
import { useState } from "react"
|
|
244
|
+
import locales from "./locales"
|
|
245
|
+
import "./tailwind.css"
|
|
246
|
+
|
|
247
|
+
interface ItemRecord {
|
|
248
|
+
id: string
|
|
249
|
+
title: string
|
|
250
|
+
createdAt: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default function Addon({ mountPoint, roomId }: AddonProps) {
|
|
254
|
+
const sdk = useAddonSDK()
|
|
255
|
+
const { t, dir } = useTranslation(locales)
|
|
256
|
+
const [title, setTitle] = useState("")
|
|
257
|
+
|
|
258
|
+
const { data, refetch, isLoading } = useAddonQuery<{ items: ItemRecord[] }>({
|
|
259
|
+
path: \`/items?roomId=\${roomId ?? ""}\`
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const { mutate: createItem, isPending } = useAddonMutation<
|
|
263
|
+
{ id: string; title: string },
|
|
264
|
+
{ roomId: string; title: string }
|
|
265
|
+
>({
|
|
266
|
+
path: "/items",
|
|
267
|
+
onSuccess: () => {
|
|
268
|
+
setTitle("")
|
|
269
|
+
refetch()
|
|
270
|
+
sdk.toast(t("toast_message", { name: "${e}" }))
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
if (mountPoint === "chat-sidebar") {
|
|
275
|
+
return (
|
|
276
|
+
<aside dir={dir} className="flex h-full flex-col border-s border-border bg-card p-4 text-card-foreground">
|
|
277
|
+
<header className="mb-4 flex items-center gap-2 border-b border-border pb-2">
|
|
278
|
+
<Icons.Plus aria-hidden="true" className="size-5 text-primary" />
|
|
279
|
+
<h2 className="font-semibold">${e}</h2>
|
|
280
|
+
</header>
|
|
281
|
+
<p className="mb-3 text-xs text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
|
|
282
|
+
<form
|
|
283
|
+
className="mb-4 flex gap-2"
|
|
284
|
+
onSubmit={(e) => {
|
|
285
|
+
e.preventDefault()
|
|
286
|
+
if (!title.trim() || !roomId) return
|
|
287
|
+
createItem({ roomId, title: title.trim() })
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<input
|
|
291
|
+
type="text"
|
|
292
|
+
value={title}
|
|
293
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
294
|
+
placeholder="Add new item..."
|
|
295
|
+
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"
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
type="submit"
|
|
299
|
+
disabled={isPending || !title.trim()}
|
|
300
|
+
className="rounded bg-primary px-3 py-1 text-sm text-primary-foreground disabled:opacity-50"
|
|
301
|
+
>
|
|
302
|
+
{isPending ? "..." : t("test_button")}
|
|
303
|
+
</button>
|
|
304
|
+
</form>
|
|
305
|
+
<div className="flex-1 space-y-1 overflow-y-auto">
|
|
306
|
+
{isLoading && <p className="text-xs text-muted-foreground">Loading...</p>}
|
|
307
|
+
{data?.items?.map((item) => (
|
|
308
|
+
<div key={item.id} className="rounded border border-border/50 bg-background/50 p-2 text-xs">
|
|
309
|
+
{item.title}
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
</aside>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div dir={dir} className="rounded border border-border bg-card p-2 text-card-foreground">
|
|
319
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
320
|
+
<div className="flex items-center gap-2">
|
|
321
|
+
<Icons.Plus aria-hidden="true" className="size-4 text-primary" />
|
|
322
|
+
<strong>${e}</strong>
|
|
323
|
+
</div>
|
|
324
|
+
<span className="text-xs text-muted-foreground">{t("mounted_at", { mountPoint })}</span>
|
|
325
|
+
</div>
|
|
326
|
+
<div className="flex gap-2">
|
|
327
|
+
<input
|
|
328
|
+
type="text"
|
|
329
|
+
value={title}
|
|
330
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
331
|
+
placeholder="New item..."
|
|
332
|
+
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"
|
|
333
|
+
/>
|
|
334
|
+
<button
|
|
335
|
+
type="button"
|
|
336
|
+
disabled={isPending || !title.trim()}
|
|
337
|
+
className="rounded bg-primary px-3 py-1 text-xs text-primary-foreground disabled:opacity-50"
|
|
338
|
+
onClick={() => {
|
|
339
|
+
if (!title.trim()) return
|
|
340
|
+
createItem({ roomId: roomId ?? "general", title: title.trim() })
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
{isPending ? "..." : t("test_button")}
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
`:`import type { AddonProps } from "@jchat/addon-sdk"
|
|
350
|
+
import { useAddonSDK, useTranslation } from "@jchat/addon-sdk"
|
|
351
|
+
import Icons from "@jchat/addon-sdk/icon"
|
|
126
352
|
import locales from "./locales"
|
|
127
353
|
import "./tailwind.css"
|
|
128
354
|
|
|
129
355
|
export default function Addon({ mountPoint }: AddonProps) {
|
|
130
356
|
const sdk = useAddonSDK()
|
|
131
|
-
const { Icons } = useIcons()
|
|
132
357
|
const { t, dir } = useTranslation(locales)
|
|
133
358
|
|
|
359
|
+
if (mountPoint === "chat-sidebar") {
|
|
360
|
+
return (
|
|
361
|
+
<aside dir={dir} className="flex h-full flex-col border-s border-border bg-card p-4 text-card-foreground">
|
|
362
|
+
<header className="mb-4 flex items-center gap-2 border-b border-border pb-2">
|
|
363
|
+
<Icons.Plus aria-hidden="true" className="size-5 text-primary" />
|
|
364
|
+
<h2 className="font-semibold">${e}</h2>
|
|
365
|
+
</header>
|
|
366
|
+
<p className="mb-2 text-sm text-muted-foreground">{t("mounted_at", { mountPoint })}</p>
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
className="mt-4 rounded bg-primary px-3 py-2 text-primary-foreground"
|
|
370
|
+
onClick={() => sdk.toast(t("toast_message", { name: "${e}" }))}
|
|
371
|
+
>
|
|
372
|
+
{t("test_button")}
|
|
373
|
+
</button>
|
|
374
|
+
</aside>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
134
378
|
return (
|
|
135
379
|
<div dir={dir} className="rounded border border-border bg-card p-2 text-card-foreground">
|
|
136
380
|
<div className="mb-2 flex items-center gap-2">
|
|
@@ -148,7 +392,7 @@ export default function Addon({ mountPoint }: AddonProps) {
|
|
|
148
392
|
</div>
|
|
149
393
|
)
|
|
150
394
|
}
|
|
151
|
-
`}function
|
|
395
|
+
`}function D(){return`import * as fs from "node:fs"
|
|
152
396
|
import * as path from "node:path"
|
|
153
397
|
|
|
154
398
|
type Command = "run" | "test" | "reset" | "package" | "cleanup"
|
|
@@ -217,7 +461,246 @@ function readEnvironment(file: string): Record<string, string> {
|
|
|
217
461
|
})
|
|
218
462
|
)
|
|
219
463
|
}
|
|
220
|
-
`}function
|
|
464
|
+
`}function O(){return`---
|
|
465
|
+
name: jchat-addon-development
|
|
466
|
+
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.
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
# JChat Addon Development Skill
|
|
470
|
+
|
|
471
|
+
Use this skill when developing, updating, or debugging JChat addons using \`@jchat/addon-sdk\`.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## 1. Architectural Guardrails (Non-Negotiable)
|
|
476
|
+
|
|
477
|
+
1. **Import Boundaries**:
|
|
478
|
+
- 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\`.
|
|
479
|
+
- NEVER import private internal monorepo packages (\`@jchat/icon\`, \`@jchat/ui\`, \`@jchat/db\`, \`@jchat/shared\`, etc.).
|
|
480
|
+
- Host externals (\`react\`, \`react-dom\`, \`@tanstack/react-query\`) are provided at runtime by the JChat host.
|
|
481
|
+
2. **Icons**:
|
|
482
|
+
- Use direct imports: \`import Icons from "@jchat/addon-sdk/icon"\`.
|
|
483
|
+
- Never call \`useIcons()\` or install \`lucide-react\` / \`react-icons\` directly in the addon.
|
|
484
|
+
3. **Database Isolation**:
|
|
485
|
+
- All addon tables MUST reside in the isolated \`addons\` PostgreSQL schema (e.g. \`addons.GlimpseStories\`).
|
|
486
|
+
- Never write to or alter core tables (\`Users\`, \`Rooms\`, \`Messages\`, \`Sites\`).
|
|
487
|
+
4. **Real-Time Event Namespacing**:
|
|
488
|
+
- All WebSocket/Redis events MUST follow the canonical format: \`addon:<addonId>:<eventName>\` (e.g. \`addon:pollwise:voted\`).
|
|
489
|
+
5. **Localization (i18n)**:
|
|
490
|
+
- All addons must support all 20 host languages:
|
|
491
|
+
\`ar\`, \`bg\`, \`de\`, \`el\`, \`en\`, \`es\`, \`fr\`, \`he\`, \`hi\`, \`hr\`, \`kn\`, \`ml\`, \`nl\`, \`pt\`, \`ro\`, \`ru\`, \`ta\`, \`te\`, \`tr\`, \`ur\`.
|
|
492
|
+
- All locale dictionaries in \`src/locales/*.json\` must maintain 100% key parity.
|
|
493
|
+
6. **Directional Styling (RTL/LTR)**:
|
|
494
|
+
- Use Tailwind CSS logical properties (\`ps-*\`, \`pe-*\`, \`ms-*\`, \`me-*\`, \`text-start\`, \`text-end\`, \`start-*\`, \`end-*\`, \`rounded-s-*\`, \`rounded-e-*\`).
|
|
495
|
+
- Never use physical \`pl-*\`, \`pr-*\`, \`ml-*\`, \`mr-*\`, \`text-left\`, or \`text-right\`.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 2. Manifest Contract (\`src/manifest.ts\`)
|
|
500
|
+
|
|
501
|
+
Define addon metadata and capabilities with \`defineAddonManifest\`:
|
|
502
|
+
|
|
503
|
+
\`\`\`typescript
|
|
504
|
+
import { defineAddonManifest } from "@jchat/addon-sdk"
|
|
505
|
+
|
|
506
|
+
export default defineAddonManifest({
|
|
507
|
+
id: "pollwise",
|
|
508
|
+
name: "Pollwise Polls",
|
|
509
|
+
version: "1.0.0",
|
|
510
|
+
description: "Create and vote on live interactive polls in chat rooms.",
|
|
511
|
+
author: "JChat Ecosystem Team",
|
|
512
|
+
compatibility: {
|
|
513
|
+
jchat: ">=1.0.0-alpha.0 <2.0.0",
|
|
514
|
+
addonSdk: ">=1.0.0-alpha.0 <2.0.0"
|
|
515
|
+
},
|
|
516
|
+
locales: [
|
|
517
|
+
"ar", "bg", "de", "el", "en", "es", "fr", "he", "hi", "hr",
|
|
518
|
+
"kn", "ml", "nl", "pt", "ro", "ru", "ta", "te", "tr", "ur"
|
|
519
|
+
],
|
|
520
|
+
defaultLocale: "en",
|
|
521
|
+
frontend: {
|
|
522
|
+
bundle: "./ui.js",
|
|
523
|
+
mountPoints: ["chat-toolbar", "room-settings"]
|
|
524
|
+
},
|
|
525
|
+
backend: {
|
|
526
|
+
bundle: "./server.js"
|
|
527
|
+
},
|
|
528
|
+
permissions: ["network", "storage"]
|
|
529
|
+
})
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## 3. Frontend UI Development (\`src/ui.tsx\`)
|
|
535
|
+
|
|
536
|
+
Addon UI components receive \`AddonProps\` and use hooks from \`@jchat/addon-sdk\`:
|
|
537
|
+
|
|
538
|
+
\`\`\`tsx
|
|
539
|
+
import {
|
|
540
|
+
type AddonProps,
|
|
541
|
+
useAddonSDK,
|
|
542
|
+
useAddonQuery,
|
|
543
|
+
useAddonMutation,
|
|
544
|
+
useAddonTranslation
|
|
545
|
+
} from "@jchat/addon-sdk"
|
|
546
|
+
import Icons from "@jchat/addon-sdk/icon"
|
|
547
|
+
|
|
548
|
+
export default function PollwiseAddon({ mountPoint, roomId }: AddonProps) {
|
|
549
|
+
const sdk = useAddonSDK()
|
|
550
|
+
const { t } = useAddonTranslation()
|
|
551
|
+
|
|
552
|
+
// 1. Data fetching via TanStack Query bridge
|
|
553
|
+
const { data: poll, isLoading } = useAddonQuery({
|
|
554
|
+
queryKey: ["poll", roomId],
|
|
555
|
+
queryFn: () => sdk.api.get<{ id: string; question: string }>(\`/rooms/\${roomId}/poll\`)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
// 2. Mutations
|
|
559
|
+
const voteMutation = useAddonMutation({
|
|
560
|
+
mutationFn: (optionId: string) => sdk.api.post(\`/rooms/\${roomId}/vote\`, { optionId }),
|
|
561
|
+
onSuccess: () => {
|
|
562
|
+
sdk.ui.toast({
|
|
563
|
+
title: t("vote_success_title"),
|
|
564
|
+
description: t("vote_success_desc"),
|
|
565
|
+
variant: "success"
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
// 3. Real-time event listener
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
const unsubscribe = sdk.events.on("addon:pollwise:updated", (updatedPoll) => {
|
|
573
|
+
// Handle incoming real-time payload
|
|
574
|
+
})
|
|
575
|
+
return () => unsubscribe()
|
|
576
|
+
}, [sdk])
|
|
577
|
+
|
|
578
|
+
if (isLoading) return <div>{t("loading")}</div>
|
|
579
|
+
|
|
580
|
+
return (
|
|
581
|
+
<div className="p-4 rounded-xl bg-card border border-border">
|
|
582
|
+
<div className="flex items-center gap-2 mb-3">
|
|
583
|
+
<Icons.BarChart3 className="size-5 text-primary" />
|
|
584
|
+
<h3 className="font-semibold text-foreground">{poll?.question}</h3>
|
|
585
|
+
</div>
|
|
586
|
+
<button
|
|
587
|
+
onClick={() => voteMutation.mutate("option-1")}
|
|
588
|
+
className="px-3 py-1.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
589
|
+
>
|
|
590
|
+
{t("vote_button")}
|
|
591
|
+
</button>
|
|
592
|
+
</div>
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
\`\`\`
|
|
596
|
+
|
|
597
|
+
### Direct Icon Usage
|
|
598
|
+
Icons are rendered directly:
|
|
599
|
+
\`\`\`tsx
|
|
600
|
+
import Icons, { resolveIcon } from "@jchat/addon-sdk/icon"
|
|
601
|
+
|
|
602
|
+
// Direct JSX:
|
|
603
|
+
<Icons.Sparkles className="size-4 text-primary" />
|
|
604
|
+
<Icons.Send className="size-5" />
|
|
605
|
+
|
|
606
|
+
// Dynamic resolution:
|
|
607
|
+
const DynamicIcon = resolveIcon("Smile", Icons.HelpCircle)
|
|
608
|
+
<DynamicIcon className="size-4" />
|
|
609
|
+
\`\`\`
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## 4. Backend Server Development (\`src/server.ts\`)
|
|
614
|
+
|
|
615
|
+
Fullstack addons expose ElysiaJS routes running within the JChat server runtime:
|
|
616
|
+
|
|
617
|
+
\`\`\`typescript
|
|
618
|
+
import { createAddonServer } from "@jchat/addon-sdk/server"
|
|
619
|
+
import { t } from "elysia"
|
|
620
|
+
|
|
621
|
+
export default createAddonServer("pollwise", ({ app, db, events }) => {
|
|
622
|
+
return app
|
|
623
|
+
.get("/rooms/:roomId/poll", async ({ params: { roomId } }) => {
|
|
624
|
+
// Query isolated addons schema using Kysely
|
|
625
|
+
const poll = await db
|
|
626
|
+
.selectFrom("addons.Polls")
|
|
627
|
+
.selectAll()
|
|
628
|
+
.where("roomId", "=", roomId)
|
|
629
|
+
.executeTakeFirst()
|
|
630
|
+
|
|
631
|
+
return poll ?? { message: "No active poll" }
|
|
632
|
+
})
|
|
633
|
+
.post(
|
|
634
|
+
"/rooms/:roomId/vote",
|
|
635
|
+
async ({ params: { roomId }, body, user }) => {
|
|
636
|
+
// Record vote in DB
|
|
637
|
+
await db
|
|
638
|
+
.insertInto("addons.PollVotes")
|
|
639
|
+
.values({
|
|
640
|
+
pollId: body.pollId,
|
|
641
|
+
userId: user.id,
|
|
642
|
+
optionId: body.optionId
|
|
643
|
+
})
|
|
644
|
+
.execute()
|
|
645
|
+
|
|
646
|
+
// Broadcast real-time event to room subscribers
|
|
647
|
+
await events.publish(\`addon:pollwise:updated\`, {
|
|
648
|
+
roomId,
|
|
649
|
+
pollId: body.pollId
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
return { success: true }
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
body: t.Object({
|
|
656
|
+
pollId: t.String(),
|
|
657
|
+
optionId: t.String()
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
})
|
|
662
|
+
\`\`\`
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## 5. Addon Localization (\`src/locales/\`)
|
|
667
|
+
|
|
668
|
+
Every translatable string in the UI must use \`t("key")\`.
|
|
669
|
+
Translations reside in \`src/locales/<lang>.json\` for all 20 supported languages:
|
|
670
|
+
|
|
671
|
+
\`\`\`json
|
|
672
|
+
// src/locales/en.json
|
|
673
|
+
{
|
|
674
|
+
"welcome": "Welcome to Pollwise",
|
|
675
|
+
"vote_button": "Submit Vote",
|
|
676
|
+
"vote_success_title": "Vote Recorded",
|
|
677
|
+
"vote_success_desc": "Thank you for participating."
|
|
678
|
+
}
|
|
679
|
+
\`\`\`
|
|
680
|
+
|
|
681
|
+
Validate key parity across all 20 languages:
|
|
682
|
+
\`\`\`bash
|
|
683
|
+
bun check:translations
|
|
684
|
+
\`\`\`
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## 6. Build and Verification Workflow
|
|
689
|
+
|
|
690
|
+
\`\`\`bash
|
|
691
|
+
# 1. Typecheck the addon
|
|
692
|
+
bun run typecheck
|
|
693
|
+
|
|
694
|
+
# 2. Bundle the addon artifacts (frontend bundle into ui.js, backend into server.js)
|
|
695
|
+
bun run build
|
|
696
|
+
|
|
697
|
+
# 3. In the standalone devkit: start the disposable sandbox environment
|
|
698
|
+
bun run devkit:start
|
|
699
|
+
|
|
700
|
+
# 4. View sandbox logs
|
|
701
|
+
bun run devkit:logs
|
|
702
|
+
\`\`\`
|
|
703
|
+
`}function k(e){return`# ${A(e)} Development Kit
|
|
221
704
|
|
|
222
705
|
This workspace develops \`${e}\` against the license-free JChat developer sandbox without
|
|
223
706
|
including JChat Core source.
|
|
@@ -243,4 +726,4 @@ Open \`http://127.0.0.1:4317\`. The HTTP fixture recorder is available at
|
|
|
243
726
|
|
|
244
727
|
The referenced JChat packages and sandbox image must be published before this standalone kit can
|
|
245
728
|
install or start outside the JChat repository.
|
|
246
|
-
`}function
|
|
729
|
+
`}function A(e){return e.split(`-`).map(e=>`${e[0]?.toUpperCase()??``}${e.slice(1)}`).join(` `)}export{c as a,A as i,s as n,u as o,l as r,i as t};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
targetDir: string;
|
|
5
|
-
jchatVersion: string;
|
|
6
|
-
addonSdkVersion: string;
|
|
7
|
-
sandboxImage?: string;
|
|
8
|
-
}
|
|
9
|
-
interface AddonWorkspaceOptions {
|
|
10
|
-
addonId: string;
|
|
11
|
-
targetDir: string;
|
|
12
|
-
isMonorepoWorkspace?: boolean;
|
|
13
|
-
jchatVersion?: string;
|
|
14
|
-
addonSdkVersion?: string;
|
|
15
|
-
}
|
|
16
|
-
declare function createAddonFiles(options: {
|
|
17
|
-
addonId: string;
|
|
18
|
-
isMonorepoWorkspace?: boolean;
|
|
19
|
-
jchatVersion?: string;
|
|
20
|
-
addonSdkVersion?: string;
|
|
21
|
-
}): Readonly<Record<string, string>>;
|
|
22
|
-
declare function writeAddonWorkspace(options: AddonWorkspaceOptions): Promise<void>;
|
|
23
|
-
declare function createStandaloneDevkitFiles(options: StandaloneDevkitOptions): Readonly<Record<string, string>>;
|
|
24
|
-
declare function writeStandaloneDevkit(options: StandaloneDevkitOptions): Promise<void>;
|
|
25
|
-
//#endregion
|
|
26
|
-
export { type AddonWorkspaceOptions, type StandaloneDevkitOptions, createAddonFiles, createStandaloneDevkitFiles, writeAddonWorkspace, writeStandaloneDevkit };
|
|
1
|
+
import { n as MountPoint, t as MOUNT_POINTS } from "./contracts.mjs";
|
|
2
|
+
import { a as createAddonFiles, c as writeAddonWorkspace, i as StandaloneDevkitOptions, l as writeStandaloneDevkit, n as AddonWorkspaceOptions, o as createStandaloneDevkitFiles, r as HOST_LOCALES, s as toDisplayName, t as AddonTemplateType } from "./generator.mjs";
|
|
3
|
+
export { type AddonTemplateType, type AddonWorkspaceOptions, HOST_LOCALES, MOUNT_POINTS, type MountPoint, type StandaloneDevkitOptions, createAddonFiles, createStandaloneDevkitFiles, toDisplayName, writeAddonWorkspace, writeStandaloneDevkit };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{t as e}from"./contracts.d.mts";import{a as t,i as n,n as r,o as i,r as a,t as o}from"./generator.mjs";export{o as HOST_LOCALES,e as MOUNT_POINTS,r as createAddonFiles,a as createStandaloneDevkitFiles,n as toDisplayName,t as writeAddonWorkspace,i as writeStandaloneDevkit};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jchat/create-addon",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tool for jchat addon creation.",
|
|
6
6
|
"keywords": [
|
|
@@ -27,15 +27,24 @@
|
|
|
27
27
|
"CHANGELOG.md"
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
|
-
"main": "./
|
|
31
|
-
"types": "./
|
|
30
|
+
"main": "./src/index.ts",
|
|
31
|
+
"types": "./src/index.ts",
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
|
-
"types": "./
|
|
35
|
-
"import": "./
|
|
34
|
+
"types": "./src/index.ts",
|
|
35
|
+
"import": "./src/index.ts",
|
|
36
|
+
"default": "./src/index.ts"
|
|
36
37
|
}
|
|
37
38
|
},
|
|
38
39
|
"publishConfig": {
|
|
40
|
+
"main": "./dist/index.mjs",
|
|
41
|
+
"types": "./dist/index.d.mts",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.mts",
|
|
45
|
+
"import": "./dist/index.mjs"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
39
48
|
"access": "public"
|
|
40
49
|
},
|
|
41
50
|
"scripts": {
|
|
@@ -46,10 +55,13 @@
|
|
|
46
55
|
"typecheck": "tsc --noEmit",
|
|
47
56
|
"prepack": "bun run build"
|
|
48
57
|
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@clack/prompts": "catalog:"
|
|
60
|
+
},
|
|
49
61
|
"devDependencies": {
|
|
50
62
|
"@jchat/addon-sdk": "workspace:*",
|
|
51
63
|
"@types/bun": "catalog:",
|
|
52
|
-
"tsdown": "
|
|
64
|
+
"tsdown": "catalog:"
|
|
53
65
|
},
|
|
54
66
|
"engines": {
|
|
55
67
|
"bun": ">=1.4.0"
|