popii-framework 0.6.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +159 -0
  2. package/dist/cli/commands.d.ts +15 -0
  3. package/dist/cli/commands.d.ts.map +1 -0
  4. package/dist/cli/pages.d.ts +5 -0
  5. package/dist/cli/pages.d.ts.map +1 -0
  6. package/dist/cli/scaffold.d.ts +23 -0
  7. package/dist/cli/scaffold.d.ts.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +1710 -0
  11. package/dist/client.d.ts +40 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/define/command.d.ts +47 -0
  14. package/dist/define/command.d.ts.map +1 -0
  15. package/dist/define/event.d.ts +52 -0
  16. package/dist/define/event.d.ts.map +1 -0
  17. package/dist/define/snap.d.ts +55 -0
  18. package/dist/define/snap.d.ts.map +1 -0
  19. package/dist/define/task.d.ts +64 -0
  20. package/dist/define/task.d.ts.map +1 -0
  21. package/dist/handler/cache.d.ts +6 -0
  22. package/dist/handler/cache.d.ts.map +1 -0
  23. package/dist/handler/context.d.ts +3 -0
  24. package/dist/handler/context.d.ts.map +1 -0
  25. package/dist/handler/index.d.ts +8 -0
  26. package/dist/handler/index.d.ts.map +1 -0
  27. package/dist/handler/interaction.d.ts +4 -0
  28. package/dist/handler/interaction.d.ts.map +1 -0
  29. package/dist/handler/lock.d.ts +2 -0
  30. package/dist/handler/lock.d.ts.map +1 -0
  31. package/dist/handler/logger.d.ts +4 -0
  32. package/dist/handler/logger.d.ts.map +1 -0
  33. package/dist/handler/message.d.ts +4 -0
  34. package/dist/handler/message.d.ts.map +1 -0
  35. package/dist/handler/middleware.d.ts +5 -0
  36. package/dist/handler/middleware.d.ts.map +1 -0
  37. package/dist/index.d.ts +31 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +10464 -0
  40. package/dist/loader/fizz.d.ts +2 -0
  41. package/dist/loader/fizz.d.ts.map +1 -0
  42. package/dist/loader/index.d.ts +3 -0
  43. package/dist/loader/index.d.ts.map +1 -0
  44. package/dist/loader/locales.d.ts +2 -0
  45. package/dist/loader/locales.d.ts.map +1 -0
  46. package/dist/loader/middleware.d.ts +54 -0
  47. package/dist/loader/middleware.d.ts.map +1 -0
  48. package/dist/loader/snaps.d.ts +3 -0
  49. package/dist/loader/snaps.d.ts.map +1 -0
  50. package/dist/loader/tasks.d.ts +3 -0
  51. package/dist/loader/tasks.d.ts.map +1 -0
  52. package/dist/plugins/activity.d.ts +15 -0
  53. package/dist/plugins/activity.d.ts.map +1 -0
  54. package/dist/plugins/ai.d.ts +35 -0
  55. package/dist/plugins/ai.d.ts.map +1 -0
  56. package/dist/plugins/automod.d.ts +12 -0
  57. package/dist/plugins/automod.d.ts.map +1 -0
  58. package/dist/plugins/canvas.d.ts +75 -0
  59. package/dist/plugins/canvas.d.ts.map +1 -0
  60. package/dist/plugins/captcha.d.ts +20 -0
  61. package/dist/plugins/captcha.d.ts.map +1 -0
  62. package/dist/plugins/commandAnalytic.d.ts +8 -0
  63. package/dist/plugins/commandAnalytic.d.ts.map +1 -0
  64. package/dist/plugins/commandLogger.d.ts +13 -0
  65. package/dist/plugins/commandLogger.d.ts.map +1 -0
  66. package/dist/plugins/desk.d.ts +23 -0
  67. package/dist/plugins/desk.d.ts.map +1 -0
  68. package/dist/plugins/economy.d.ts +41 -0
  69. package/dist/plugins/economy.d.ts.map +1 -0
  70. package/dist/plugins/errorHandler.d.ts +8 -0
  71. package/dist/plugins/errorHandler.d.ts.map +1 -0
  72. package/dist/plugins/giveaway.d.ts +21 -0
  73. package/dist/plugins/giveaway.d.ts.map +1 -0
  74. package/dist/plugins/lastfm.d.ts +24 -0
  75. package/dist/plugins/lastfm.d.ts.map +1 -0
  76. package/dist/plugins/mongoose.d.ts +23 -0
  77. package/dist/plugins/mongoose.d.ts.map +1 -0
  78. package/dist/plugins/pay.d.ts +24 -0
  79. package/dist/plugins/pay.d.ts.map +1 -0
  80. package/dist/plugins/permissionGuard.d.ts +8 -0
  81. package/dist/plugins/permissionGuard.d.ts.map +1 -0
  82. package/dist/plugins/reload.d.ts +8 -0
  83. package/dist/plugins/reload.d.ts.map +1 -0
  84. package/dist/plugins/sqlite.d.ts +20 -0
  85. package/dist/plugins/sqlite.d.ts.map +1 -0
  86. package/dist/plugins/telemetry.d.ts +11 -0
  87. package/dist/plugins/telemetry.d.ts.map +1 -0
  88. package/dist/plugins/ui.d.ts +60 -0
  89. package/dist/plugins/ui.d.ts.map +1 -0
  90. package/dist/plugins/voice/player.d.ts +9 -0
  91. package/dist/plugins/voice/player.d.ts.map +1 -0
  92. package/dist/plugins/voice/resolver.d.ts +16 -0
  93. package/dist/plugins/voice/resolver.d.ts.map +1 -0
  94. package/dist/plugins/voice/setup.d.ts +11 -0
  95. package/dist/plugins/voice/setup.d.ts.map +1 -0
  96. package/dist/plugins/voice/sponsorblock.d.ts +17 -0
  97. package/dist/plugins/voice/sponsorblock.d.ts.map +1 -0
  98. package/dist/plugins/voice/state.d.ts +35 -0
  99. package/dist/plugins/voice/state.d.ts.map +1 -0
  100. package/dist/plugins/voice/types.d.ts +14 -0
  101. package/dist/plugins/voice/types.d.ts.map +1 -0
  102. package/dist/plugins/voice/utils.d.ts +14 -0
  103. package/dist/plugins/voice/utils.d.ts.map +1 -0
  104. package/dist/plugins/voice.d.ts +72 -0
  105. package/dist/plugins/voice.d.ts.map +1 -0
  106. package/dist/plugins/web/helpers.d.ts +17 -0
  107. package/dist/plugins/web/helpers.d.ts.map +1 -0
  108. package/dist/plugins/web/index.d.ts +16 -0
  109. package/dist/plugins/web/index.d.ts.map +1 -0
  110. package/dist/plugins/web/routes/auth.d.ts +3 -0
  111. package/dist/plugins/web/routes/auth.d.ts.map +1 -0
  112. package/dist/plugins/web/routes/dashboard.d.ts +3 -0
  113. package/dist/plugins/web/routes/dashboard.d.ts.map +1 -0
  114. package/dist/plugins/web/routes/database.d.ts +3 -0
  115. package/dist/plugins/web/routes/database.d.ts.map +1 -0
  116. package/dist/plugins/web/routes/locales.d.ts +3 -0
  117. package/dist/plugins/web/routes/locales.d.ts.map +1 -0
  118. package/dist/plugins/web/routes/members.d.ts +3 -0
  119. package/dist/plugins/web/routes/members.d.ts.map +1 -0
  120. package/dist/plugins/web/routes/metrics.d.ts +7 -0
  121. package/dist/plugins/web/routes/metrics.d.ts.map +1 -0
  122. package/dist/plugins/web/routes/music.d.ts +3 -0
  123. package/dist/plugins/web/routes/music.d.ts.map +1 -0
  124. package/dist/plugins/web/routes/servers.d.ts +3 -0
  125. package/dist/plugins/web/routes/servers.d.ts.map +1 -0
  126. package/dist/plugins/web/routes/tasks.d.ts +3 -0
  127. package/dist/plugins/web/routes/tasks.d.ts.map +1 -0
  128. package/dist/plugins/web/routes/ui-editor.d.ts +3 -0
  129. package/dist/plugins/web/routes/ui-editor.d.ts.map +1 -0
  130. package/dist/plugins/web/setup.d.ts +7 -0
  131. package/dist/plugins/web/setup.d.ts.map +1 -0
  132. package/dist/plugins/web/types.d.ts +60 -0
  133. package/dist/plugins/web/types.d.ts.map +1 -0
  134. package/dist/plugins/web/views/admin.d.ts +5 -0
  135. package/dist/plugins/web/views/admin.d.ts.map +1 -0
  136. package/dist/plugins/web/views/base.d.ts +13 -0
  137. package/dist/plugins/web/views/base.d.ts.map +1 -0
  138. package/dist/plugins/web/views/commands.d.ts +2 -0
  139. package/dist/plugins/web/views/commands.d.ts.map +1 -0
  140. package/dist/plugins/web/views/dashboard.d.ts +32 -0
  141. package/dist/plugins/web/views/dashboard.d.ts.map +1 -0
  142. package/dist/plugins/web/views/database.d.ts +2 -0
  143. package/dist/plugins/web/views/database.d.ts.map +1 -0
  144. package/dist/plugins/web/views/home.d.ts +2 -0
  145. package/dist/plugins/web/views/home.d.ts.map +1 -0
  146. package/dist/plugins/web/views/index.d.ts +16 -0
  147. package/dist/plugins/web/views/index.d.ts.map +1 -0
  148. package/dist/plugins/web/views/invite.d.ts +2 -0
  149. package/dist/plugins/web/views/invite.d.ts.map +1 -0
  150. package/dist/plugins/web/views/lastfm.d.ts +2 -0
  151. package/dist/plugins/web/views/lastfm.d.ts.map +1 -0
  152. package/dist/plugins/web/views/locales.d.ts +2 -0
  153. package/dist/plugins/web/views/locales.d.ts.map +1 -0
  154. package/dist/plugins/web/views/members.d.ts +6 -0
  155. package/dist/plugins/web/views/members.d.ts.map +1 -0
  156. package/dist/plugins/web/views/music.d.ts +5 -0
  157. package/dist/plugins/web/views/music.d.ts.map +1 -0
  158. package/dist/plugins/web/views/servers.d.ts +3 -0
  159. package/dist/plugins/web/views/servers.d.ts.map +1 -0
  160. package/dist/plugins/web/views/terminal.d.ts +2 -0
  161. package/dist/plugins/web/views/terminal.d.ts.map +1 -0
  162. package/dist/plugins/web/views/ui-editor.d.ts +2 -0
  163. package/dist/plugins/web/views/ui-editor.d.ts.map +1 -0
  164. package/dist/types.d.ts +671 -0
  165. package/dist/types.d.ts.map +1 -0
  166. package/dist/utils/error.d.ts +62 -0
  167. package/dist/utils/error.d.ts.map +1 -0
  168. package/dist/utils/help.d.ts +3 -0
  169. package/dist/utils/help.d.ts.map +1 -0
  170. package/dist/utils/logger.d.ts +1 -0
  171. package/dist/utils/logger.d.ts.map +1 -0
  172. package/dist/utils/sharding.d.ts +17 -0
  173. package/dist/utils/sharding.d.ts.map +1 -0
  174. package/dist/utils/testing.d.ts +42 -0
  175. package/dist/utils/testing.d.ts.map +1 -0
  176. package/dist/utils/testing.js +192 -0
  177. package/package.json +77 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1710 @@
1
+ #!/usr/bin/env bun
2
+ import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
+
31
+ // src/cli/commands.ts
32
+ import { intro, text, multiselect, confirm, outro, isCancel, spinner, select } from "@clack/prompts";
33
+ import { mkdir as mkdir2 } from "node:fs/promises";
34
+ import { join as join2, dirname } from "node:path";
35
+ import { pathToFileURL } from "node:url";
36
+ import { existsSync, watch, readdirSync, readFileSync } from "node:fs";
37
+
38
+ // src/cli/scaffold.ts
39
+ import { mkdir, rm } from "node:fs/promises";
40
+ import { join } from "node:path";
41
+ var BUILTIN_PLUGIN_FIELDS = {
42
+ webPlugin: [
43
+ { configKey: "port", type: "number", default: "3000" },
44
+ { configKey: "publicUrl", type: "string" },
45
+ { configKey: "dashboard", type: "bool" },
46
+ { configKey: "dashboardPassword", envKey: "WEB_DASHBOARD_PASSWORD", type: "string" },
47
+ { configKey: "oauth2.clientId", envKey: "DISCORD_CLIENT_ID", type: "string" },
48
+ { configKey: "oauth2.clientSecret", envKey: "DISCORD_CLIENT_SECRET", type: "string" },
49
+ { configKey: "oauth2.redirectUri", envKey: "REDIRECT_URI", type: "string" }
50
+ ],
51
+ mongoosePlugin: [{ configKey: "uri", envKey: "MONGODB_URI", type: "string" }],
52
+ sqlitePlugin: [{ configKey: "filename", type: "string", default: "db.sqlite" }],
53
+ popiiAiPlugin: [
54
+ { configKey: "provider", type: "string", default: "anthropic" },
55
+ { configKey: "apiKey", envKey: "AI_API_KEY", type: "string" },
56
+ { configKey: "model", type: "string" }
57
+ ],
58
+ commandLoggerPlugin: [
59
+ { configKey: "logExecution", type: "bool", default: "false" },
60
+ { configKey: "logCompletion", type: "bool", default: "false" },
61
+ { configKey: "logErrors", type: "bool", default: "true" }
62
+ ],
63
+ economyPlugin: [
64
+ { configKey: "currencyName", type: "string", default: "Coins" },
65
+ { configKey: "cooldownMs", type: "number", default: "60000" }
66
+ ],
67
+ captchaPlugin: [
68
+ { configKey: "siteKey", envKey: "HCAPTCHA_SITE_KEY", type: "string" },
69
+ { configKey: "secretKey", envKey: "HCAPTCHA_SECRET_KEY", type: "string" },
70
+ { configKey: "port", type: "number", default: "3001" },
71
+ { configKey: "publicUrl", type: "string" }
72
+ ],
73
+ activityRotatorPlugin: [
74
+ { configKey: "intervalMs", type: "number", default: "60000" }
75
+ ],
76
+ payPlugin: [
77
+ { configKey: "patreon.webhookSecret", envKey: "PATREON_WEBHOOK_SECRET", type: "string", enabledBy: "patreon.enabled" },
78
+ { configKey: "kofi.verificationToken", envKey: "KOFI_VERIFICATION_TOKEN", type: "string", enabledBy: "kofi.enabled" }
79
+ ]
80
+ };
81
+ function buildPluginOptionsStr(pluginName, cfg) {
82
+ if (pluginName === "payPlugin") {
83
+ const parts = [];
84
+ if (cfg["patreon.enabled"] === "true" && cfg["patreon.webhookSecret"]) {
85
+ const port = parseInt(cfg["patreon.port"] || "8080");
86
+ parts.push(`patreon: { webhookSecret: process.env.PATREON_WEBHOOK_SECRET || "", port: ${port} }`);
87
+ }
88
+ if (cfg["kofi.enabled"] === "true" && cfg["kofi.verificationToken"]) {
89
+ const port = parseInt(cfg["kofi.port"] || "8081");
90
+ parts.push(`kofi: { verificationToken: process.env.KOFI_VERIFICATION_TOKEN || "", port: ${port} }`);
91
+ }
92
+ return parts.join(", ");
93
+ }
94
+ if (pluginName === "activityRotatorPlugin") {
95
+ const intervalMs = parseInt(cfg["intervalMs"] || "60000");
96
+ const name = cfg["activity.name"] || "with popii";
97
+ const typeMap = { Playing: 0, Streaming: 1, Listening: 2, Watching: 3, Competing: 5 };
98
+ const type = typeMap[cfg["activity.type"] ?? "Playing"] ?? 0;
99
+ return `intervalMs: ${intervalMs}, activities: [{ name: "${name}", type: ${type} }]`;
100
+ }
101
+ const specs = BUILTIN_PLUGIN_FIELDS[pluginName];
102
+ if (!specs)
103
+ return "";
104
+ const top = [];
105
+ const nested = {};
106
+ for (const spec of specs) {
107
+ const raw = cfg[spec.configKey] ?? spec.default ?? "";
108
+ if (!raw)
109
+ continue;
110
+ let code;
111
+ if (spec.envKey) {
112
+ if (!cfg[spec.configKey])
113
+ continue;
114
+ code = `process.env.${spec.envKey} || ""`;
115
+ } else if (spec.type === "number") {
116
+ code = String(parseInt(raw) || 0);
117
+ } else if (spec.type === "bool") {
118
+ code = raw === "true" ? "true" : "false";
119
+ } else {
120
+ code = `"${raw}"`;
121
+ }
122
+ const parts = spec.configKey.split(".");
123
+ if (parts.length === 1)
124
+ top.push(`${spec.configKey}: ${code}`);
125
+ else {
126
+ const [parent, ...rest] = parts;
127
+ (nested[parent] ??= []).push(`${rest.join(".")}: ${code}`);
128
+ }
129
+ }
130
+ for (const [k, entries] of Object.entries(nested))
131
+ if (entries.length)
132
+ top.push(`${k}: { ${entries.join(", ")} }`);
133
+ return top.join(", ");
134
+ }
135
+ async function scaffoldProject(opts) {
136
+ const { botName, plugins, communityPlugins = [], botToken, installDeps, genDocker, pluginConfig, communityPluginConfig } = opts;
137
+ let djsVersion = "^14.26.3";
138
+ try {
139
+ const djsRes = await fetch("https://registry.npmjs.org/discord.js/latest");
140
+ if (djsRes.ok) {
141
+ const djsData = await djsRes.json();
142
+ if (djsData.version)
143
+ djsVersion = `^${djsData.version}`;
144
+ }
145
+ } catch {}
146
+ const targetDir = join(process.cwd(), botName);
147
+ console.log(` Scaffolding project at: ${targetDir}`);
148
+ await mkdir(targetDir, { recursive: true });
149
+ try {
150
+ await mkdir(join(targetDir, "src", "commands"), { recursive: true });
151
+ await mkdir(join(targetDir, "src", "events"), { recursive: true });
152
+ await mkdir(join(targetDir, "src", "snaps"), { recursive: true });
153
+ await mkdir(join(targetDir, "src", "middlewares"), { recursive: true });
154
+ await mkdir(join(targetDir, "src", "tasks"), { recursive: true });
155
+ const communityDeps = {};
156
+ for (const cp of communityPlugins)
157
+ communityDeps[cp.npmName] = "latest";
158
+ const pluginPeerDeps = {
159
+ voicePlugin: { "@discordjs/voice": "latest" },
160
+ canvasPlugin: { "@napi-rs/canvas": "latest" },
161
+ lastFmPlugin: { "@discordjs/voice": "latest" },
162
+ webPlugin: { ejs: "latest" },
163
+ mongoosePlugin: { mongoose: "latest" }
164
+ };
165
+ const builtinDeps = {};
166
+ for (const p of plugins)
167
+ Object.assign(builtinDeps, pluginPeerDeps[p] ?? {});
168
+ const pkgJson = {
169
+ name: botName,
170
+ version: "1.0.0",
171
+ private: true,
172
+ scripts: { dev: "popii dev", start: "bun src/index.ts" },
173
+ dependencies: { popii: "latest", "discord.js": djsVersion, ...builtinDeps, ...communityDeps },
174
+ devDependencies: { "@types/bun": "latest" }
175
+ };
176
+ await Bun.write(join(targetDir, "package.json"), JSON.stringify(pkgJson, null, 2));
177
+ const tsConfig = {
178
+ compilerOptions: {
179
+ target: "ESNext",
180
+ module: "ESNext",
181
+ moduleResolution: "Bundler",
182
+ strict: true,
183
+ skipLibCheck: true,
184
+ types: ["bun"]
185
+ }
186
+ };
187
+ await Bun.write(join(targetDir, "tsconfig.json"), JSON.stringify(tsConfig, null, 2));
188
+ const envLines = [`DISCORD_TOKEN=${botToken}`];
189
+ for (const p of plugins) {
190
+ const cfg = pluginConfig?.[p] ?? {};
191
+ for (const spec of BUILTIN_PLUGIN_FIELDS[p] ?? []) {
192
+ if (!spec.envKey)
193
+ continue;
194
+ if (spec.enabledBy && cfg[spec.enabledBy] !== "true")
195
+ continue;
196
+ const val = cfg[spec.configKey] ?? "";
197
+ envLines.push(`${spec.envKey}=${val}`);
198
+ }
199
+ }
200
+ for (const fields of Object.values(communityPluginConfig ?? {}))
201
+ for (const [k, v] of Object.entries(fields))
202
+ envLines.push(`${k}=${v}`);
203
+ await Bun.write(join(targetDir, ".env"), envLines.join(`
204
+ `) + `
205
+ `);
206
+ if (genDocker) {
207
+ const dockerfile = `FROM oven/bun:latest
208
+ WORKDIR /app
209
+ RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg python3 && apt-get clean && rm -rf /var/lib/apt/lists/*
210
+ COPY package.json bun.lockb* ./
211
+ RUN bun install --production
212
+ COPY . .
213
+ USER bun
214
+ HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/health || exit 1
215
+ CMD ["bun", "run", "start"]
216
+ `;
217
+ await Bun.write(join(targetDir, "Dockerfile"), dockerfile);
218
+ await Bun.write(join(targetDir, "docker-compose.yml"), `version: '3.8'
219
+ services:
220
+ bot:
221
+ build: .
222
+ env_file: .env
223
+ restart: unless-stopped
224
+ `);
225
+ }
226
+ const builtinImport = plugins.length > 0 ? `import { popiiClient, ${plugins.join(", ")} } from "popii";` : `import { popiiClient } from "popii";`;
227
+ const communityImportLines = communityPlugins.map((cp) => `import { ${cp.exportName} } from "${cp.npmName}";`).join(`
228
+ `);
229
+ const imports = communityImportLines ? `${builtinImport}
230
+ ${communityImportLines}` : builtinImport;
231
+ const pluginInit = [
232
+ ...plugins.map((p) => {
233
+ const cfg = pluginConfig?.[p] ?? {};
234
+ const opts2 = buildPluginOptionsStr(p, cfg);
235
+ return ` ${p}(${opts2 ? `{ ${opts2} }` : ""}),`;
236
+ }),
237
+ ...communityPlugins.map((cp) => ` ${cp.exportName}({ /* options */ }),`)
238
+ ].join(`
239
+ `);
240
+ const indexTs = `${imports}
241
+
242
+ const client = popiiClient({
243
+ token: process.env.DISCORD_TOKEN || "",
244
+ prefix: "!",
245
+ owners: [],
246
+ plugins: [
247
+ ${pluginInit}
248
+ ]
249
+ });
250
+
251
+ client.start().then(() => console.log("Bot is online!")).catch(console.error);
252
+
253
+ process.on("SIGINT", async () => {
254
+ console.log("\\nShutting down...");
255
+ await client.stop();
256
+ process.exit(0);
257
+ });
258
+ `;
259
+ await Bun.write(join(targetDir, "src", "index.ts"), indexTs);
260
+ const pingTs = `import { command } from "popii";
261
+
262
+ export default command({
263
+ name: "ping",
264
+ description: "Replies with pong!",
265
+ slash: true,
266
+ text: true,
267
+ async do(pop) {
268
+ await pop.reply(\`Pong! Latency: \${pop.client.discord.ws.ping}ms\`);
269
+ }
270
+ });
271
+ `;
272
+ await Bun.write(join(targetDir, "src", "commands", "ping.ts"), pingTs);
273
+ if (installDeps) {
274
+ const proc = Bun.spawn(["bun", "install"], { cwd: targetDir, stdout: "ignore", stderr: "pipe" });
275
+ const exitCode = await proc.exited;
276
+ if (exitCode !== 0) {
277
+ const errText = await new Response(proc.stderr).text();
278
+ throw new Error(`bun install failed:
279
+ ${errText}`);
280
+ }
281
+ }
282
+ } catch (err) {
283
+ await rm(targetDir, { recursive: true, force: true });
284
+ throw err;
285
+ }
286
+ }
287
+
288
+ // src/cli/pages.ts
289
+ function openBrowser(url) {
290
+ if (process.platform === "win32")
291
+ Bun.spawn(["cmd", "/c", "start", "", url]);
292
+ else if (process.platform === "darwin")
293
+ Bun.spawn(["open", url]);
294
+ else
295
+ Bun.spawn(["xdg-open", url]);
296
+ }
297
+ function setupPageHtml() {
298
+ const pluginDefs = [
299
+ ["uiPlugin", "UI", ".paginate(), .prompt(), .form()"],
300
+ ["sqlitePlugin", "SQLite", "Bun SQLite database in context"],
301
+ ["errorHandlerPlugin", "Error Handler", "Catches PopiiErrors gracefully"],
302
+ ["commandLoggerPlugin", "Command Logger", "Logs command executions"],
303
+ ["commandAnalyticPlugin", "Analytics", "Tracks command usage stats"],
304
+ ["permissionGuardPlugin", "Permissions", "Enforces permissions automatically"],
305
+ ["reloadPlugin", "Hot Reload", "Adds a /reload command"],
306
+ ["webPlugin", "Web Server", "/health and /metrics endpoints"],
307
+ ["telemetryPlugin", "Telemetry", "Performance monitoring"],
308
+ ["voicePlugin", "Voice", "Voice channel support"],
309
+ ["mongoosePlugin", "Mongoose", "MongoDB via Mongoose ODM"],
310
+ ["deskPlugin", "Desk", "Support ticket system"],
311
+ ["popiiAiPlugin", "AI", "Claude AI integration"],
312
+ ["payPlugin", "Pay", "Payment processing"],
313
+ ["autoModPlugin", "AutoMod", "Automated moderation"],
314
+ ["economyPlugin", "Economy", "Virtual currency system"],
315
+ ["giveawayPlugin", "Giveaway", "Run giveaways in channels"],
316
+ ["activityRotatorPlugin", "Activity Rotator", "Rotates bot status messages"],
317
+ ["captchaPlugin", "Captcha", "Verification captchas"],
318
+ ["canvasPlugin", "Canvas", "Image generation"],
319
+ ["lastFmPlugin", "Last.fm", "Last.fm music integration"]
320
+ ];
321
+ const pluginCards = pluginDefs.map(([value, name, desc]) => `
322
+ <div class="plugin-card" onclick="toggleCard(this)">
323
+ <input type="checkbox" name="plugins" value="${value}">
324
+ <div class="check-box"><svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
325
+ <div class="plugin-info"><div class="name">${name}</div><div class="desc">${desc}</div></div>
326
+ </div>`).join("");
327
+ return `<!DOCTYPE html>
328
+ <html lang="en">
329
+ <head>
330
+ <meta charset="utf-8">
331
+ <meta name="viewport" content="width=device-width, initial-scale=1">
332
+ <title>Popii Setup</title>
333
+ <style>
334
+ :root {
335
+ color-scheme: dark;
336
+ --accent: #F04F96; --accent-glow: rgba(240,79,150,0.45); --accent-light: #FF80BA;
337
+ --bg-dark: #0D0610; --bg-mid: #1a0920; --bg-light: #2a1030;
338
+ --card-bg: rgba(220,80,140,0.055); --card-border: rgba(240,79,150,0.12);
339
+ --card-highlight: rgba(255,160,200,0.08); --text-main: #fdf0f6;
340
+ --text-muted: rgba(255,195,220,0.6);
341
+ }
342
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
343
+ body {
344
+ background: radial-gradient(ellipse 80% 60% at 15% 10%, var(--bg-light), transparent),
345
+ radial-gradient(ellipse 55% 45% at 85% 90%, #200d2e, transparent),
346
+ var(--bg-dark);
347
+ background-attachment: fixed;
348
+ color: var(--text-main);
349
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
350
+ min-height: 100vh;
351
+ display: flex; flex-direction: column; align-items: center;
352
+ padding: 3.5rem 1rem 5rem;
353
+ }
354
+ header { text-align: center; margin-bottom: 2.5rem; }
355
+ header h1 {
356
+ font-size: 2.8rem; font-weight: 800; letter-spacing: -0.05em;
357
+ background: linear-gradient(135deg, var(--accent), var(--accent-light));
358
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
359
+ margin-bottom: 0.4rem;
360
+ }
361
+ header p { color: var(--text-muted); font-size: 1rem; }
362
+ form, #success { width: 100%; max-width: 580px; }
363
+ form { display: flex; flex-direction: column; gap: 1.75rem; }
364
+ .field { display: flex; flex-direction: column; gap: 0.5rem; }
365
+ .field > label { font-weight: 600; font-size: 0.9rem; }
366
+ .hint { color: var(--text-muted); font-weight: 400; margin-left: 0.35rem; }
367
+ input[type="text"], input[type="password"] {
368
+ background: rgba(255,255,255,0.04); border: 1px solid var(--card-border);
369
+ border-radius: 12px; color: var(--text-main); font-family: inherit;
370
+ font-size: 1rem; padding: 0.75rem 1rem; outline: none; width: 100%;
371
+ transition: border-color 0.2s, box-shadow 0.2s;
372
+ }
373
+ input[type="text"]:focus, input[type="password"]:focus {
374
+ border-color: var(--accent); box-shadow: 0 0 0 3px rgba(240,79,150,0.2);
375
+ }
376
+ .token-wrap { position: relative; }
377
+ .token-wrap input { padding-right: 3rem; }
378
+ .token-toggle {
379
+ position: absolute; right: 0.75rem; top: 50%; transform: translateY(-50%);
380
+ background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 0.25rem;
381
+ }
382
+ .plugins-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; max-height: 380px; overflow-y: auto; padding-right: 4px; }
383
+ .plugins-grid::-webkit-scrollbar { width: 4px; }
384
+ .plugins-grid::-webkit-scrollbar-track { background: transparent; }
385
+ .plugins-grid::-webkit-scrollbar-thumb { background: var(--card-border); border-radius: 4px; }
386
+ .plugin-card, .opt-card {
387
+ background: var(--card-bg); border: 1px solid var(--card-border);
388
+ border-radius: 14px; padding: 0.85rem 1rem; cursor: pointer;
389
+ transition: all 0.15s; display: flex; align-items: flex-start; gap: 0.65rem;
390
+ user-select: none;
391
+ }
392
+ .plugin-card:hover, .opt-card:hover { border-color: rgba(240,79,150,0.3); background: rgba(240,79,150,0.07); }
393
+ .plugin-card.selected, .opt-card.selected {
394
+ border-color: var(--accent); background: rgba(240,79,150,0.12);
395
+ box-shadow: 0 0 0 1px rgba(240,79,150,0.2);
396
+ }
397
+ .plugin-card input, .opt-card input { display: none; }
398
+ .check-box {
399
+ width: 18px; height: 18px; border-radius: 5px; border: 2px solid var(--card-border);
400
+ flex-shrink: 0; margin-top: 1px; display: flex; align-items: center; justify-content: center;
401
+ transition: all 0.15s;
402
+ }
403
+ .plugin-card.selected .check-box, .opt-card.selected .check-box {
404
+ background: var(--accent); border-color: var(--accent);
405
+ }
406
+ .check-box svg { display: none; }
407
+ .plugin-card.selected .check-box svg, .opt-card.selected .check-box svg { display: block; }
408
+ .plugin-info .name, .opt-info .name { font-weight: 600; font-size: 0.88rem; }
409
+ .plugin-info .desc, .opt-info .desc { color: var(--text-muted); font-size: 0.77rem; margin-top: 0.2rem; line-height: 1.3; }
410
+ .opts-row { display: flex; gap: 0.75rem; }
411
+ .opt-card { flex: 1; align-items: center; }
412
+ .opt-card .check-box { margin-top: 0; }
413
+ .submit-btn {
414
+ width: 100%; background: linear-gradient(135deg, var(--accent), var(--accent-light));
415
+ color: #fff; border: none; border-radius: 14px; padding: 0.9rem;
416
+ font-size: 1rem; font-weight: 700; font-family: inherit; cursor: pointer;
417
+ transition: all 0.2s; box-shadow: 0 6px 20px rgba(240,79,150,0.35); letter-spacing: 0.01em;
418
+ }
419
+ .submit-btn:hover:not(:disabled) { box-shadow: 0 8px 28px rgba(240,79,150,0.5); transform: translateY(-2px); }
420
+ .submit-btn:disabled { opacity: 0.6; cursor: not-allowed; }
421
+ .field-error { color: #ED4245; font-size: 0.83rem; padding-top: 0.25rem; min-height: 1.1rem; }
422
+ .creating { text-align: center; padding: 3rem 0; }
423
+ .spinner { width: 40px; height: 40px; border: 3px solid var(--card-border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 1rem; }
424
+ @keyframes spin { to { transform: rotate(360deg); } }
425
+ .config-group { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 14px; padding: 1rem 1.25rem; margin-bottom: 0.65rem; }
426
+ .config-group:last-child { margin-bottom: 0; }
427
+ .config-group-label { font-weight: 700; font-size: 0.78rem; color: var(--accent-light); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 0.75rem; }
428
+ .config-fields { display: flex; flex-direction: column; gap: 0.6rem; }
429
+ .config-field { display: flex; flex-direction: column; gap: 0.3rem; }
430
+ .config-field > label { font-size: 0.83rem; font-weight: 500; }
431
+ .config-input, .config-select { background: rgba(255,255,255,0.04); border: 1px solid var(--card-border); border-radius: 8px; color: var(--text-main); font-family: inherit; font-size: 0.88rem; padding: 0.5rem 0.75rem; outline: none; width: 100%; transition: border-color 0.2s; }
432
+ .config-input:focus, .config-select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(240,79,150,0.15); }
433
+ .config-select option { background: var(--bg-dark); }
434
+ .config-check-label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.83rem; font-weight: 500; }
435
+ .config-check-label input[type="checkbox"] { display: revert; accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }
436
+ </style>
437
+ </head>
438
+ <body>
439
+ <header>
440
+ <h1>popii</h1>
441
+ <p>Let's set up your new bot</p>
442
+ </header>
443
+
444
+ <form id="setup-form">
445
+ <div class="field">
446
+ <label for="bot-name">Bot name<span class="hint">— used as the project folder name</span></label>
447
+ <input type="text" id="bot-name" placeholder="my-popii-bot" autocomplete="off" spellcheck="false">
448
+ <div class="field-error" id="name-error"></div>
449
+ </div>
450
+
451
+ <div class="field">
452
+ <label>Plugins<span class="hint">— optional, can be added later</span></label>
453
+ <div class="plugins-grid">${pluginCards}
454
+ </div>
455
+ </div>
456
+
457
+ <div class="field" id="community-section" style="display:none">
458
+ <label>Community plugins<span class="hint">— from the Popii registry</span></label>
459
+ <div id="community-loading" style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0">Loading registry...</div>
460
+ <div class="plugins-grid" id="community-grid"></div>
461
+ </div>
462
+
463
+ <div class="field" id="plugin-config-section" style="display:none">
464
+ <label>Plugin configuration<span class="hint">— can be changed in src/index.ts and .env later</span></label>
465
+ <div id="plugin-config-container"></div>
466
+ </div>
467
+
468
+ <div class="field">
469
+ <label for="bot-token">Discord bot token<span class="hint">— leave blank to add later</span></label>
470
+ <div class="token-wrap">
471
+ <input type="password" id="bot-token" placeholder="MTAx..." autocomplete="off" spellcheck="false">
472
+ <button type="button" class="token-toggle" id="token-toggle" title="Show/hide token">
473
+ <svg id="eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
474
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
475
+ </svg>
476
+ </button>
477
+ </div>
478
+ </div>
479
+
480
+ <div class="field">
481
+ <label>Options</label>
482
+ <div class="opts-row">
483
+ <div class="opt-card selected" onclick="toggleCard(this)">
484
+ <input type="checkbox" id="install-deps" checked>
485
+ <div class="check-box"><svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
486
+ <div class="opt-info"><div class="name">Install deps</div><div class="desc">Run bun install</div></div>
487
+ </div>
488
+ <div class="opt-card" onclick="toggleCard(this)">
489
+ <input type="checkbox" id="gen-docker">
490
+ <div class="check-box"><svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
491
+ <div class="opt-info"><div class="name">Docker files</div><div class="desc">Dockerfile + compose</div></div>
492
+ </div>
493
+ </div>
494
+ </div>
495
+
496
+ <div>
497
+ <button type="submit" class="submit-btn" id="submit-btn">Create bot project</button>
498
+ <div class="field-error" id="submit-error"></div>
499
+ </div>
500
+ </form>
501
+
502
+ <div id="creating" class="creating" style="display:none">
503
+ <div class="spinner"></div>
504
+ <p style="color:var(--text-muted)">Creating your project…</p>
505
+ </div>
506
+
507
+ <script>
508
+ var PLUGIN_CONFIG_SCHEMA = {
509
+ webPlugin: { label: 'Web Server', fields: [
510
+ { key: 'port', label: 'Port', type: 'number', default: '3000' },
511
+ { key: 'publicUrl', label: 'Public URL', type: 'text', placeholder: 'https://bot.example.com' },
512
+ { key: 'dashboard', label: 'Enable dashboard', type: 'checkbox' },
513
+ { key: 'dashboardPassword', label: 'Dashboard password', type: 'password', showIf: 'dashboard' },
514
+ { key: 'oauth2.clientId', label: 'Client ID', type: 'text', showIf: 'dashboard', hint: 'Developer Portal → OAuth2' },
515
+ { key: 'oauth2.clientSecret', label: 'Client secret', type: 'password', showIf: 'dashboard' },
516
+ { key: 'oauth2.redirectUri', label: 'Redirect URI', type: 'text', showIf: 'dashboard' },
517
+ ]},
518
+ mongoosePlugin: { label: 'Mongoose', fields: [
519
+ { key: 'uri', label: 'MongoDB URI', type: 'text', placeholder: 'mongodb://localhost:27017/mybot' },
520
+ ]},
521
+ sqlitePlugin: { label: 'SQLite', fields: [
522
+ { key: 'filename', label: 'Database filename', type: 'text', default: 'db.sqlite' },
523
+ ]},
524
+ popiiAiPlugin: { label: 'AI', fields: [
525
+ { key: 'provider', label: 'Provider', type: 'select', options: ['anthropic', 'openai', 'gemini'] },
526
+ { key: 'apiKey', label: 'API Key', type: 'password' },
527
+ { key: 'model', label: 'Model (optional)', type: 'text' },
528
+ ]},
529
+ commandLoggerPlugin: { label: 'Command Logger', fields: [
530
+ { key: 'logExecution', label: 'Log execution', type: 'checkbox' },
531
+ { key: 'logCompletion', label: 'Log completion', type: 'checkbox' },
532
+ { key: 'logErrors', label: 'Log errors', type: 'checkbox', default: true },
533
+ ]},
534
+ economyPlugin: { label: 'Economy', fields: [
535
+ { key: 'currencyName', label: 'Currency name', type: 'text', default: 'Coins' },
536
+ { key: 'cooldownMs', label: 'Daily cooldown (ms)', type: 'number', default: '60000' },
537
+ ]},
538
+ captchaPlugin: { label: 'Captcha', fields: [
539
+ { key: 'siteKey', label: 'hCaptcha site key', type: 'text', hint: 'dashboard.hcaptcha.com' },
540
+ { key: 'secretKey', label: 'hCaptcha secret key', type: 'password' },
541
+ { key: 'port', label: 'Verification server port', type: 'number', default: '3001' },
542
+ { key: 'publicUrl', label: 'Public URL (optional)', type: 'text', placeholder: 'https://verify.example.com' },
543
+ ]},
544
+ activityRotatorPlugin: { label: 'Activity Rotator', fields: [
545
+ { key: 'intervalMs', label: 'Rotation interval (ms)', type: 'number', default: '60000' },
546
+ { key: 'activity.name', label: 'Status text', type: 'text', default: 'with popii' },
547
+ { key: 'activity.type', label: 'Activity type', type: 'select', options: ['Playing', 'Listening', 'Watching', 'Competing', 'Streaming'] },
548
+ ]},
549
+ payPlugin: { label: 'Pay', fields: [
550
+ { key: 'patreon.enabled', label: 'Enable Patreon', type: 'checkbox' },
551
+ { key: 'patreon.webhookSecret', label: 'Patreon webhook secret', type: 'password', showIf: 'patreon.enabled' },
552
+ { key: 'patreon.port', label: 'Patreon webhook port', type: 'number', default: '8080', showIf: 'patreon.enabled' },
553
+ { key: 'kofi.enabled', label: 'Enable Ko-fi', type: 'checkbox' },
554
+ { key: 'kofi.verificationToken', label: 'Ko-fi verification token', type: 'password', showIf: 'kofi.enabled' },
555
+ { key: 'kofi.port', label: 'Ko-fi webhook port', type: 'number', default: '8081', showIf: 'kofi.enabled' },
556
+ ]},
557
+ };
558
+
559
+ var communityConfigCache = {};
560
+
561
+ function renderConfigField(namePrefix, field) {
562
+ var name = namePrefix + '[' + field.key + ']';
563
+ var showIf = field.showIf ? ' data-show-if="' + field.showIf + '"' : '';
564
+ var hide = field.showIf ? ' style="display:none"' : '';
565
+ if (field.type === 'checkbox') {
566
+ var chk = field.default ? ' checked' : '';
567
+ return '<div class="config-field"' + showIf + hide + '><label class="config-check-label"><input type="checkbox" name="' + name + '" value="true"' + chk + '><span>' + field.label + '</span></label></div>';
568
+ }
569
+ if (field.type === 'select') {
570
+ var opts = (field.options || []).map(function(o) { return '<option value="' + o + '">' + o + '</option>'; }).join('');
571
+ return '<div class="config-field"' + showIf + hide + '><label>' + field.label + '</label><select name="' + name + '" class="config-select">' + opts + '</select></div>';
572
+ }
573
+ var hint = field.hint ? '<span class="hint"> — ' + field.hint + '</span>' : '';
574
+ var ph = field.placeholder ? ' placeholder="' + field.placeholder + '"' : '';
575
+ var val = field.default ? ' value="' + field.default + '"' : '';
576
+ return '<div class="config-field"' + showIf + hide + '><label>' + field.label + hint + '</label><input type="' + (field.type || 'text') + '" name="' + name + '" class="config-input"' + ph + val + '></div>';
577
+ }
578
+
579
+ function wireShowIf(group, namePrefix) {
580
+ group.querySelectorAll('[data-show-if]').forEach(function(el) {
581
+ var cb = group.querySelector('input[name="' + namePrefix + '[' + el.getAttribute('data-show-if') + ']"]');
582
+ if (!cb) return;
583
+ function update() { el.style.display = cb.checked ? '' : 'none'; }
584
+ cb.addEventListener('change', update);
585
+ update();
586
+ });
587
+ }
588
+
589
+ function addBuiltinConfigGroup(pluginName) {
590
+ var schema = PLUGIN_CONFIG_SCHEMA[pluginName];
591
+ if (!schema) return;
592
+ var container = document.getElementById('plugin-config-container');
593
+ if (container.querySelector('[data-plugin="' + pluginName + '"]')) return;
594
+ var prefix = 'pluginConfig[' + pluginName + ']';
595
+ var html = schema.fields.map(function(f) { return renderConfigField(prefix, f); }).join('');
596
+ var group = document.createElement('div');
597
+ group.className = 'config-group';
598
+ group.dataset.plugin = pluginName;
599
+ group.innerHTML = '<div class="config-group-label">' + schema.label + '</div><div class="config-fields">' + html + '</div>';
600
+ container.appendChild(group);
601
+ wireShowIf(group, prefix);
602
+ updateConfigSectionVisibility();
603
+ }
604
+
605
+ function removeConfigGroup(key) {
606
+ var el = document.querySelector('#plugin-config-container [data-plugin="' + key + '"]');
607
+ if (el) { el.remove(); updateConfigSectionVisibility(); }
608
+ }
609
+
610
+ function updateConfigSectionVisibility() {
611
+ var c = document.getElementById('plugin-config-container');
612
+ document.getElementById('plugin-config-section').style.display = c.children.length ? '' : 'none';
613
+ }
614
+
615
+ async function fetchCommunityPluginConfig(npmName) {
616
+ var key = 'community:' + npmName;
617
+ var container = document.getElementById('plugin-config-container');
618
+ if (container.querySelector('[data-plugin="' + key + '"]')) return;
619
+ if (!(npmName in communityConfigCache)) {
620
+ try {
621
+ var r = await fetch('https://registry.npmjs.org/' + encodeURIComponent(npmName) + '/latest');
622
+ communityConfigCache[npmName] = r.ok ? (((await r.json()).popii) || {}).configSchema || null : null;
623
+ } catch(e) { communityConfigCache[npmName] = null; }
624
+ }
625
+ var schema = communityConfigCache[npmName];
626
+ if (!schema) return;
627
+ var envFields = Object.entries(schema).filter(function(e) { return e[1] && e[1].env; });
628
+ if (!envFields.length) return;
629
+ var prefix = 'communityPluginConfig[' + npmName + ']';
630
+ var html = envFields.map(function(entry) {
631
+ var envKey = entry[0], spec = entry[1];
632
+ return renderConfigField(prefix, { key: envKey, label: spec.label || envKey, type: spec.secret ? 'password' : 'text', placeholder: spec.placeholder || '', default: spec.default != null ? String(spec.default) : '' });
633
+ }).join('');
634
+ var group = document.createElement('div');
635
+ group.className = 'config-group';
636
+ group.dataset.plugin = key;
637
+ group.innerHTML = '<div class="config-group-label">' + npmName.replace('popii-plugin-', '') + '</div><div class="config-fields">' + html + '</div>';
638
+ container.appendChild(group);
639
+ updateConfigSectionVisibility();
640
+ }
641
+
642
+ function collectPluginConfig() {
643
+ var pluginConfig = {}, communityPluginConfig = {};
644
+ document.querySelectorAll('#plugin-config-container .config-group').forEach(function(group) {
645
+ var plugin = group.dataset.plugin;
646
+ var isCommunity = plugin.startsWith('community:');
647
+ var cfg = {};
648
+ group.querySelectorAll('input[name], select[name]').forEach(function(el) {
649
+ var lastOpen = el.name.lastIndexOf('[');
650
+ var lastClose = el.name.lastIndexOf(']');
651
+ if (lastOpen === -1 || lastClose === -1) return;
652
+ var k = el.name.slice(lastOpen + 1, lastClose);
653
+ cfg[k] = el.type === 'checkbox' ? (el.checked ? 'true' : 'false') : el.value;
654
+ });
655
+ if (isCommunity) communityPluginConfig[plugin.slice(10)] = cfg;
656
+ else pluginConfig[plugin] = cfg;
657
+ });
658
+ return { pluginConfig: pluginConfig, communityPluginConfig: communityPluginConfig };
659
+ }
660
+
661
+ function toggleCard(card) {
662
+ var cb = card.querySelector('input[type="checkbox"]');
663
+ cb.checked = !cb.checked;
664
+ card.classList.toggle('selected', cb.checked);
665
+ if (cb.name === 'plugins') {
666
+ if (cb.checked) addBuiltinConfigGroup(cb.value);
667
+ else removeConfigGroup(cb.value);
668
+ } else if (cb.name === 'community-plugins') {
669
+ var npm = cb.value.split(':')[0];
670
+ if (cb.checked) fetchCommunityPluginConfig(npm);
671
+ else removeConfigGroup('community:' + npm);
672
+ }
673
+ }
674
+
675
+ var tokenInput = document.getElementById('bot-token');
676
+ document.getElementById('token-toggle').addEventListener('click', function() {
677
+ var show = tokenInput.type === 'password';
678
+ tokenInput.type = show ? 'text' : 'password';
679
+ document.getElementById('eye-icon').innerHTML = show
680
+ ? '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>'
681
+ : '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>';
682
+ });
683
+
684
+ document.getElementById('setup-form').addEventListener('submit', async function(e) {
685
+ e.preventDefault();
686
+ var name = document.getElementById('bot-name').value.trim();
687
+ var nameErr = document.getElementById('name-error');
688
+ var submitErr = document.getElementById('submit-error');
689
+ var btn = document.getElementById('submit-btn');
690
+ nameErr.textContent = '';
691
+ submitErr.textContent = '';
692
+
693
+ if (!name) { nameErr.textContent = 'Name is required.'; return; }
694
+ if (name.toLowerCase() === 'popii') { nameErr.textContent = "Cannot be named 'popii' — causes a dependency loop."; return; }
695
+
696
+ var plugins = Array.from(document.querySelectorAll('input[name="plugins"]:checked')).map(function(el) { return el.value; });
697
+ var communityPlugins = Array.from(document.querySelectorAll('input[name="community-plugins"]:checked')).map(function(el) {
698
+ var parts = el.value.split(':');
699
+ return { npmName: parts[0], exportName: parts[1] };
700
+ });
701
+ var cfg = collectPluginConfig();
702
+ var body = {
703
+ botName: name,
704
+ plugins: plugins,
705
+ communityPlugins: communityPlugins,
706
+ botToken: document.getElementById('bot-token').value.trim(),
707
+ installDeps: document.getElementById('install-deps').checked,
708
+ genDocker: document.getElementById('gen-docker').checked,
709
+ pluginConfig: cfg.pluginConfig,
710
+ communityPluginConfig: cfg.communityPluginConfig,
711
+ };
712
+
713
+ btn.disabled = true;
714
+ document.getElementById('setup-form').style.display = 'none';
715
+ document.getElementById('creating').style.display = 'block';
716
+
717
+ try {
718
+ var res = await fetch('/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
719
+ if (!res.ok) throw new Error(await res.text());
720
+ window.location.href = '/done?name=' + encodeURIComponent(name);
721
+ } catch(err) {
722
+ document.getElementById('setup-form').style.display = 'flex';
723
+ document.getElementById('creating').style.display = 'none';
724
+ btn.disabled = false;
725
+ submitErr.textContent = 'Setup failed: ' + (err.message || err);
726
+ }
727
+ });
728
+ var CHECK_SVG = '<svg width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 4L3.5 6.5L9 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
729
+ var REGISTRY_URL = 'https://raw.githubusercontent.com/popii-dev/registry/main/plugins.json';
730
+
731
+ async function loadCommunityPlugins() {
732
+ var section = document.getElementById('community-section');
733
+ var grid = document.getElementById('community-grid');
734
+ var loading = document.getElementById('community-loading');
735
+ try {
736
+ var res = await fetch(REGISTRY_URL);
737
+ if (!res.ok) throw new Error('unavailable');
738
+ var data = await res.json();
739
+ var plugins = (data.plugins || []).filter(function(p) { return p.npm && p.displayName; });
740
+ loading.style.display = 'none';
741
+ if (plugins.length === 0) return;
742
+ for (var i = 0; i < plugins.length; i++) {
743
+ var p = plugins[i];
744
+ var exportName = p.export || (p.npm.replace('popii-plugin-', '').replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); }) + 'Plugin');
745
+ var card = document.createElement('div');
746
+ card.className = 'plugin-card';
747
+ card.onclick = (function(c) { return function() { toggleCard(c); }; })(card);
748
+ var badge = p.verified ? ' <span style="color:var(--accent);font-size:0.72rem;font-weight:700;">✓</span>' : '';
749
+ card.innerHTML = '<input type="checkbox" name="community-plugins" value="' + p.npm + ':' + exportName + '">' +
750
+ '<div class="check-box">' + CHECK_SVG + '</div>' +
751
+ '<div class="plugin-info"><div class="name">' + p.displayName + badge + '</div><div class="desc">' + (p.description || '') + '</div></div>';
752
+ grid.appendChild(card);
753
+ }
754
+ section.style.display = '';
755
+ } catch (e) {
756
+ section.style.display = 'none';
757
+ }
758
+ }
759
+
760
+ loadCommunityPlugins();
761
+ </script>
762
+ </body>
763
+ </html>`;
764
+ }
765
+ function donePageHtml(botName) {
766
+ return `<!DOCTYPE html>
767
+ <html lang="en">
768
+ <head>
769
+ <meta charset="utf-8">
770
+ <meta name="viewport" content="width=device-width, initial-scale=1">
771
+ <title>${botName} is ready!</title>
772
+ <style>
773
+ :root { color-scheme: dark; --accent: #F04F96; --accent-light: #FF80BA; --bg-dark: #0D0610; --bg-light: #2a1030; --card-bg: rgba(220,80,140,0.055); --card-border: rgba(240,79,150,0.12); --text-main: #fdf0f6; --text-muted: rgba(255,195,220,0.6); }
774
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
775
+ body { background: radial-gradient(ellipse 80% 60% at 15% 10%, var(--bg-light), transparent), var(--bg-dark); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Inter', system-ui, sans-serif; color: var(--text-main); padding: 2rem; }
776
+ .card { background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 24px; padding: 3rem 2.5rem; text-align: center; max-width: 480px; width: 100%; }
777
+ .icon { font-size: 3.5rem; margin-bottom: 1.25rem; }
778
+ h1 { font-size: 1.8rem; font-weight: 800; margin-bottom: 0.5rem; }
779
+ .sub { color: var(--text-muted); margin-bottom: 2rem; }
780
+ pre { background: rgba(0,0,0,0.35); border: 1px solid var(--card-border); border-radius: 12px; padding: 1rem 1.25rem; font-size: 0.95rem; color: var(--accent-light); font-family: monospace; line-height: 1.7; text-align: left; white-space: pre; }
781
+ .note { color: var(--text-muted); font-size: 0.82rem; margin-top: 1.5rem; }
782
+ </style>
783
+ </head>
784
+ <body>
785
+ <div class="card">
786
+ <div class="icon">✨</div>
787
+ <h1>${botName} is ready!</h1>
788
+ <p class="sub">Your project has been scaffolded. Get started:</p>
789
+ <pre>cd ${botName}
790
+ bun run dev</pre>
791
+ <p class="note">This window can be closed.</p>
792
+ </div>
793
+ </body>
794
+ </html>`;
795
+ }
796
+ async function initViaWeb() {
797
+ return new Promise((resolve) => {
798
+ const server = Bun.serve({
799
+ port: 0,
800
+ async fetch(req) {
801
+ const url = new URL(req.url);
802
+ if (url.pathname === "/" && req.method === "GET") {
803
+ return new Response(setupPageHtml(), { headers: { "Content-Type": "text/html; charset=utf-8" } });
804
+ }
805
+ if (url.pathname === "/setup" && req.method === "POST") {
806
+ try {
807
+ const body = await req.json();
808
+ await scaffoldProject(body);
809
+ return new Response(JSON.stringify({ ok: true }), { headers: { "Content-Type": "application/json" } });
810
+ } catch (err) {
811
+ console.error(`
812
+ Setup error:`, err?.message ?? err);
813
+ return new Response(err?.message ?? "Setup failed", { status: 500 });
814
+ }
815
+ }
816
+ if (url.pathname === "/done" && req.method === "GET") {
817
+ const name = url.searchParams.get("name") ?? "your-bot";
818
+ setTimeout(() => {
819
+ server.stop();
820
+ resolve();
821
+ }, 3000);
822
+ return new Response(donePageHtml(name), { headers: { "Content-Type": "text/html; charset=utf-8" } });
823
+ }
824
+ return new Response("Not found", { status: 404 });
825
+ }
826
+ });
827
+ console.log(`
828
+ Opening setup page → http://localhost:${server.port}`);
829
+ console.log(` Paste that URL manually if your browser doesn't open.
830
+ `);
831
+ openBrowser(`http://localhost:${server.port}`);
832
+ });
833
+ }
834
+
835
+ // src/cli/commands.ts
836
+ async function runDev(args) {
837
+ console.log("Starting Popii in dev mode with HMR enabled...");
838
+ globalThis.__POPII_DEV_REGISTER = (client) => {
839
+ const watchDir = (dir, callback) => {
840
+ if (existsSync(dir)) {
841
+ let timeout;
842
+ watch(dir, { recursive: true }, () => {
843
+ clearTimeout(timeout);
844
+ timeout = setTimeout(callback, 200);
845
+ });
846
+ }
847
+ };
848
+ const reload = () => client.reload().catch(console.error);
849
+ watchDir(client.config.commands?.dir ?? "./src/commands", reload);
850
+ watchDir(client.config.snaps?.dir ?? "./src/snaps", reload);
851
+ watchDir(client.config.middlewares?.dir ?? "./src/middlewares", reload);
852
+ watchDir(client.config.events?.dir ?? "./src/events", reload);
853
+ watchDir(client.config.tasks?.dir ?? "./src/tasks", reload);
854
+ };
855
+ const entryFile = args[1] || join2("src", "index.ts");
856
+ await import(pathToFileURL(join2(process.cwd(), entryFile)).href);
857
+ }
858
+ async function runSync(args) {
859
+ console.log("\uD83D\uDD04 Syncing Popii commands to Discord...");
860
+ globalThis.__POPII_SYNC_REGISTER = (client) => {
861
+ client.__syncOnly = true;
862
+ };
863
+ const entryFile = args[1] || join2("src", "index.ts");
864
+ await import(pathToFileURL(join2(process.cwd(), entryFile)).href);
865
+ }
866
+ async function runConsole(args) {
867
+ console.log("Starting Popii Interactive Console...");
868
+ globalThis.__POPII_DEV_REGISTER = (client) => {
869
+ client.discord.once("ready", () => {
870
+ const repl = __require("node:repl");
871
+ console.log(`
872
+ [Popii Console] Bot is ready! 'client', 'popii', and 'discord' are available in scope.`);
873
+ const r = repl.start("popii> ");
874
+ r.context.client = client;
875
+ r.context.popii = client;
876
+ r.context.discord = client.discord;
877
+ });
878
+ };
879
+ const entryFile = args[1] || join2("src", "index.ts");
880
+ await import(pathToFileURL(join2(process.cwd(), entryFile)).href);
881
+ }
882
+ async function runMigrate(args) {
883
+ const action = args[1];
884
+ const fs = __require("node:fs");
885
+ const path = __require("node:path");
886
+ const migrationsDir = path.join(process.cwd(), "migrations");
887
+ if (!fs.existsSync(migrationsDir))
888
+ fs.mkdirSync(migrationsDir);
889
+ if (action === "create") {
890
+ const name = args[2] || "migration";
891
+ const timestamp = Date.now();
892
+ const filename = `${timestamp}-${name}.sql`;
893
+ fs.writeFileSync(path.join(migrationsDir, filename), `-- Write your SQL migration here
894
+ `);
895
+ console.log(`✅ Created migration: migrations/${filename}`);
896
+ return;
897
+ } else if (action === "up") {
898
+ const { Database } = __require("bun:sqlite");
899
+ const db = new Database("popii.db");
900
+ db.exec("CREATE TABLE IF NOT EXISTS migrations (filename TEXT PRIMARY KEY, run_at INTEGER);");
901
+ const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
902
+ let count = 0;
903
+ for (const file of files) {
904
+ const exists = db.query("SELECT filename FROM migrations WHERE filename = ?").get(file);
905
+ if (!exists) {
906
+ console.log(`Running migration: ${file}...`);
907
+ const sql = fs.readFileSync(path.join(migrationsDir, file), "utf8");
908
+ try {
909
+ db.transaction(() => {
910
+ db.exec(sql);
911
+ db.query("INSERT INTO migrations (filename, run_at) VALUES (?, ?)").run(file, Date.now());
912
+ })();
913
+ count++;
914
+ } catch (e) {
915
+ console.error(`❌ Migration ${file} failed: ${e.message}`);
916
+ db.close();
917
+ process.exit(1);
918
+ }
919
+ }
920
+ }
921
+ console.log(`✅ Applied ${count} new migrations.`);
922
+ db.close();
923
+ return;
924
+ } else {
925
+ console.log("Usage: bunx popii migrate <create|up> [name]");
926
+ }
927
+ }
928
+ async function runInstall(args) {
929
+ const repoArg = args[1];
930
+ if (!repoArg || !repoArg.includes("/")) {
931
+ console.log("Usage: bunx popii install <user/repo[@tag]>");
932
+ process.exit(1);
933
+ }
934
+ const atIdx = repoArg.indexOf("@");
935
+ const repoPath = atIdx !== -1 ? repoArg.slice(0, atIdx) : repoArg;
936
+ const tag = atIdx !== -1 ? repoArg.slice(atIdx + 1) : "main";
937
+ const [user, name] = repoPath.split("/");
938
+ const baseUrl = `https://raw.githubusercontent.com/${user}/${name}/${tag}`;
939
+ const pluginUrl = `${baseUrl}/index.ts`;
940
+ const manifestUrl = `${baseUrl}/popii.plugin.json`;
941
+ let manifest = null;
942
+ try {
943
+ const mRes = await fetch(manifestUrl);
944
+ if (mRes.ok)
945
+ manifest = await mRes.json();
946
+ } catch {}
947
+ console.warn(`
948
+ ⚠️ WARNING: You are about to download and install code from an external source:`);
949
+ console.warn(` ${pluginUrl}`);
950
+ if (manifest) {
951
+ console.log(`
952
+ \uD83D\uDCE6 ${manifest.name ?? name} v${manifest.version ?? "?"}`);
953
+ if (manifest.description)
954
+ console.log(` ${manifest.description}`);
955
+ if (manifest.requiredEnvVars?.length) {
956
+ console.log(`
957
+ Required env vars:`);
958
+ for (const v of manifest.requiredEnvVars)
959
+ console.log(` • ${v}`);
960
+ }
961
+ if (manifest.conflicts?.length) {
962
+ console.log(`
963
+ Conflicts with: ${manifest.conflicts.join(", ")}`);
964
+ }
965
+ } else {
966
+ console.warn(` No popii.plugin.json found — this plugin is unverified.`);
967
+ }
968
+ console.warn(`
969
+ Only install plugins from authors you trust.
970
+ `);
971
+ const { confirm: confirmInstall, isCancel: isInstallCancel } = await import("@clack/prompts");
972
+ const ok = await confirmInstall({ message: "Proceed with installation?" });
973
+ if (isInstallCancel(ok) || !ok) {
974
+ console.log("Installation cancelled.");
975
+ process.exit(0);
976
+ }
977
+ const res = await fetch(pluginUrl);
978
+ if (!res.ok) {
979
+ console.error(`❌ Failed to download plugin from ${pluginUrl}`);
980
+ process.exit(1);
981
+ }
982
+ const dest = join2(process.cwd(), "src", "plugins", `${name}.ts`);
983
+ await mkdir2(dirname(dest), { recursive: true });
984
+ await Bun.write(dest, await res.text());
985
+ console.log(`✅ Installed plugin to ${dest}`);
986
+ if (manifest?.requiredEnvVars?.length) {
987
+ console.log(`
988
+ \uD83D\uDCDD Add these to your .env file:`);
989
+ for (const v of manifest.requiredEnvVars)
990
+ console.log(` ${v}=`);
991
+ console.log();
992
+ }
993
+ }
994
+ async function runAdd(args) {
995
+ const rawName = args[1];
996
+ if (!rawName) {
997
+ console.log("Usage: bunx popii add <plugin-name>");
998
+ process.exit(1);
999
+ }
1000
+ const pkgName = rawName.includes("/") || rawName.startsWith("popii-plugin-") ? rawName : `popii-plugin-${rawName}`;
1001
+ const s = spinner();
1002
+ s.start(`Looking up ${pkgName} on npm...`);
1003
+ let npmMeta = null;
1004
+ let pkgVersion = "";
1005
+ let pkgDescription = "";
1006
+ try {
1007
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkgName)}/latest`);
1008
+ if (res.ok) {
1009
+ const data = await res.json();
1010
+ npmMeta = data.popii ?? null;
1011
+ pkgVersion = data.version ?? "";
1012
+ pkgDescription = data.description ?? "";
1013
+ s.stop(`Found ${pkgName}@${pkgVersion}`);
1014
+ if (pkgDescription)
1015
+ console.log(` ${pkgDescription}`);
1016
+ if (npmMeta?.category)
1017
+ console.log(` Category: ${npmMeta.category}`);
1018
+ if (npmMeta?.requires?.length)
1019
+ console.log(` Requires: ${npmMeta.requires.join(", ")}`);
1020
+ } else {
1021
+ s.stop(`Package "${pkgName}" not found on npm.`);
1022
+ process.exit(1);
1023
+ }
1024
+ } catch {
1025
+ s.stop("Could not reach npm registry.");
1026
+ process.exit(1);
1027
+ }
1028
+ if (!npmMeta) {
1029
+ console.warn(`
1030
+ ⚠️ This package has no "popii" field — it may not be a valid Popii plugin.
1031
+ `);
1032
+ const ok = await confirm({ message: "Install anyway?" });
1033
+ if (isCancel(ok) || !ok) {
1034
+ console.log("Cancelled.");
1035
+ process.exit(0);
1036
+ }
1037
+ }
1038
+ const installS = spinner();
1039
+ installS.start(`Installing ${pkgName}...`);
1040
+ const proc = Bun.spawn(["bun", "add", pkgName], { cwd: process.cwd(), stdout: "ignore", stderr: "pipe" });
1041
+ await proc.exited;
1042
+ if (proc.exitCode !== 0) {
1043
+ const errText = await new Response(proc.stderr).text();
1044
+ installS.stop(`Failed to install ${pkgName}.`);
1045
+ if (errText)
1046
+ console.error(errText);
1047
+ process.exit(1);
1048
+ }
1049
+ installS.stop(`Installed ${pkgName}!`);
1050
+ if (npmMeta?.configSchema) {
1051
+ const envFields = Object.entries(npmMeta.configSchema).filter(([, f]) => f.env);
1052
+ if (envFields.length > 0) {
1053
+ const envPath = join2(process.cwd(), ".env");
1054
+ let envContent = existsSync(envPath) ? await Bun.file(envPath).text() : "";
1055
+ let wrote = false;
1056
+ console.log(`
1057
+ Configure environment variables:`);
1058
+ for (const [, field] of envFields) {
1059
+ const envKey = field.env;
1060
+ if (envContent.includes(`${envKey}=`))
1061
+ continue;
1062
+ const label = `${envKey}${field.required ? "" : " (optional)"}`;
1063
+ const val = await text({ message: label, placeholder: field.default != null ? String(field.default) : "" });
1064
+ if (!isCancel(val) && val !== "") {
1065
+ envContent += `
1066
+ ${envKey}=${val}`;
1067
+ wrote = true;
1068
+ }
1069
+ }
1070
+ if (wrote) {
1071
+ await Bun.write(envPath, envContent.trimStart());
1072
+ console.log(" Written to .env");
1073
+ }
1074
+ }
1075
+ }
1076
+ const exportName = npmMeta?.export ?? `${rawName.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase())}Plugin`;
1077
+ console.log(`
1078
+ Add to your bot:
1079
+ `);
1080
+ console.log(` import { ${exportName} } from "${pkgName}";`);
1081
+ console.log(` // plugins: [ ${exportName}({ /* options */ }) ]
1082
+ `);
1083
+ }
1084
+ async function runRemove(args) {
1085
+ const rawName = args[1];
1086
+ if (!rawName) {
1087
+ console.log("Usage: bunx popii remove <plugin-name>");
1088
+ process.exit(1);
1089
+ }
1090
+ const pkgName = rawName.includes("/") || rawName.startsWith("popii-plugin-") ? rawName : `popii-plugin-${rawName}`;
1091
+ const s = spinner();
1092
+ s.start(`Removing ${pkgName}...`);
1093
+ const proc = Bun.spawn(["bun", "remove", pkgName], { cwd: process.cwd(), stdout: "ignore", stderr: "ignore" });
1094
+ await proc.exited;
1095
+ if (proc.exitCode === 0) {
1096
+ s.stop(`Removed ${pkgName}.`);
1097
+ } else {
1098
+ s.stop(`Failed to remove ${pkgName}. Is it installed?`);
1099
+ process.exit(1);
1100
+ }
1101
+ }
1102
+ async function runSearch(args) {
1103
+ const query = args.slice(1).join(" ");
1104
+ if (!query) {
1105
+ console.log("Usage: bunx popii search <query>");
1106
+ process.exit(1);
1107
+ }
1108
+ const REGISTRY_URL = "https://raw.githubusercontent.com/popii-dev/registry/main/plugins.json";
1109
+ const s = spinner();
1110
+ s.start("Searching...");
1111
+ const [npmResult, registryResult] = await Promise.allSettled([
1112
+ fetch(`https://registry.npmjs.org/-/v1/search?text=keywords:popii-plugin+${encodeURIComponent(query)}&size=15`).then((r) => r.json()),
1113
+ fetch(REGISTRY_URL).then((r) => r.json())
1114
+ ]);
1115
+ s.stop("");
1116
+ const verified = new Set;
1117
+ if (registryResult.status === "fulfilled") {
1118
+ for (const p of registryResult.value.plugins ?? []) {
1119
+ if (p.verified)
1120
+ verified.add(p.npm);
1121
+ }
1122
+ }
1123
+ if (npmResult.status !== "fulfilled") {
1124
+ console.error("Could not reach npm registry.");
1125
+ process.exit(1);
1126
+ }
1127
+ const objects = npmResult.value.objects ?? [];
1128
+ if (objects.length === 0) {
1129
+ console.log(`No plugins found for "${query}". Try a different keyword.`);
1130
+ return;
1131
+ }
1132
+ console.log(`
1133
+ Results for "${query}":
1134
+ `);
1135
+ for (const { package: pkg } of objects) {
1136
+ const badge = verified.has(pkg.name) ? " ✓" : "";
1137
+ console.log(` ${pkg.name}@${pkg.version}${badge}`);
1138
+ if (pkg.description)
1139
+ console.log(` ${pkg.description}`);
1140
+ }
1141
+ console.log(`
1142
+ Install: bunx popii add <name>
1143
+ `);
1144
+ }
1145
+ async function runList(args) {
1146
+ const nmDir = join2(process.cwd(), "node_modules");
1147
+ if (!existsSync(nmDir)) {
1148
+ console.log("No node_modules found. Run bun install first.");
1149
+ return;
1150
+ }
1151
+ const found = [];
1152
+ const scanDir = (dir) => {
1153
+ let entries;
1154
+ try {
1155
+ entries = readdirSync(dir);
1156
+ } catch {
1157
+ return;
1158
+ }
1159
+ for (const entry of entries) {
1160
+ if (!entry.startsWith("popii-plugin-"))
1161
+ continue;
1162
+ const pkgJsonPath = join2(dir, entry, "package.json");
1163
+ if (!existsSync(pkgJsonPath))
1164
+ continue;
1165
+ try {
1166
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
1167
+ if (!pkg.popii)
1168
+ continue;
1169
+ found.push({
1170
+ name: pkg.name ?? entry,
1171
+ version: pkg.version ?? "?",
1172
+ description: pkg.description ?? pkg.popii?.displayName ?? ""
1173
+ });
1174
+ } catch {}
1175
+ }
1176
+ };
1177
+ scanDir(nmDir);
1178
+ for (const entry of readdirSync(nmDir)) {
1179
+ if (entry.startsWith("@"))
1180
+ scanDir(join2(nmDir, entry));
1181
+ }
1182
+ if (found.length === 0) {
1183
+ console.log(`
1184
+ No Popii plugins installed.
1185
+ Run: bunx popii search <query>
1186
+ `);
1187
+ return;
1188
+ }
1189
+ console.log(`
1190
+ Installed Popii plugins:
1191
+ `);
1192
+ for (const p of found) {
1193
+ console.log(` ${p.name}@${p.version}`);
1194
+ if (p.description)
1195
+ console.log(` ${p.description}`);
1196
+ }
1197
+ console.log();
1198
+ }
1199
+ async function runGenerate(args) {
1200
+ const type = args[1];
1201
+ const name = args[2];
1202
+ if (!type || !name) {
1203
+ console.log("Usage: bunx popii g <command|event|snap|middleware|task|test> <name>");
1204
+ process.exit(1);
1205
+ }
1206
+ if (type === "command") {
1207
+ const path = join2(process.cwd(), "src", "commands", `${name}.ts`);
1208
+ const content = `import { command } from "popii";
1209
+
1210
+ export default command({
1211
+ name: "${name.split("/").pop() ?? name}",
1212
+ description: "Description for ${name}",
1213
+ slash: true,
1214
+ do(pop) {
1215
+ pop.reply("Hello from ${name}!");
1216
+ }
1217
+ });
1218
+ `;
1219
+ await mkdir2(dirname(path), { recursive: true });
1220
+ await Bun.write(path, content);
1221
+ console.log(`✅ Created command: ${path}`);
1222
+ } else if (type === "event") {
1223
+ const path = join2(process.cwd(), "src", "events", `${name}.ts`);
1224
+ const content = `import { event } from "popii";
1225
+
1226
+ export default event<"${name}">(async (pop, ...args) => {
1227
+ // Event logic here
1228
+ });
1229
+ `;
1230
+ await mkdir2(dirname(path), { recursive: true });
1231
+ await Bun.write(path, content);
1232
+ console.log(`✅ Created event: ${path}`);
1233
+ } else if (type === "snap") {
1234
+ const path = join2(process.cwd(), "src", "snaps", `${name}.ts`);
1235
+ const content = `import { snap } from "popii";
1236
+
1237
+ export default snap({
1238
+ customId: "${name}",
1239
+ do(pop) {
1240
+ pop.reply("Snap triggered!");
1241
+ }
1242
+ });
1243
+ `;
1244
+ await mkdir2(dirname(path), { recursive: true });
1245
+ await Bun.write(path, content);
1246
+ console.log(`✅ Created snap: ${path}`);
1247
+ } else if (type === "middleware") {
1248
+ const path = join2(process.cwd(), "src", "middlewares", `${name}.ts`);
1249
+ const content = `import { middleware } from "popii";
1250
+
1251
+ export default middleware(async (pop, next) => {
1252
+ await next();
1253
+ });
1254
+ `;
1255
+ await mkdir2(dirname(path), { recursive: true });
1256
+ await Bun.write(path, content);
1257
+ console.log(`✅ Created middleware: ${path}`);
1258
+ } else if (type === "task") {
1259
+ const path = join2(process.cwd(), "src", "tasks", `${name}.ts`);
1260
+ const content = `import { task } from "popii";
1261
+
1262
+ export default task({
1263
+ name: "${name}",
1264
+ schedule: "* * * * *",
1265
+ async run(client, payload) {
1266
+ }
1267
+ });
1268
+ `;
1269
+ await mkdir2(dirname(path), { recursive: true });
1270
+ await Bun.write(path, content);
1271
+ console.log(`✅ Created task: ${path}`);
1272
+ } else if (type === "test") {
1273
+ const parts = name.split("/");
1274
+ const prefix = "../".repeat(parts.length);
1275
+ const commandPath = join2(process.cwd(), "src", "commands", `${name}.ts`);
1276
+ const snapPath = join2(process.cwd(), "src", "snaps", `${name}.ts`);
1277
+ const isSnap = !existsSync(commandPath) && existsSync(snapPath);
1278
+ const label = parts[parts.length - 1];
1279
+ const testPath = join2(process.cwd(), "test", `${name}.test.ts`);
1280
+ let content;
1281
+ if (isSnap) {
1282
+ content = `import { describe, it, expect } from "bun:test";
1283
+ import { createMockPop } from "popii/testing";
1284
+ import snap from "${prefix}src/snaps/${name}";
1285
+
1286
+ describe("${label} snap", () => {
1287
+ it("replies when triggered", async () => {
1288
+ const { pop, replies } = createMockPop();
1289
+ await snap.do(pop as any);
1290
+ expect(replies.length).toBeGreaterThan(0);
1291
+ });
1292
+ });
1293
+ `;
1294
+ } else {
1295
+ content = `import { describe, it, expect } from "bun:test";
1296
+ import { createMockPop } from "popii/testing";
1297
+ import command from "${prefix}src/commands/${name}";
1298
+
1299
+ describe("/${label}", () => {
1300
+ it("replies when invoked", async () => {
1301
+ const { pop, replies } = createMockPop();
1302
+ await command.do(pop);
1303
+ expect(replies.length).toBeGreaterThan(0);
1304
+ });
1305
+
1306
+ it("handles options", async () => {
1307
+ const { pop, replies } = createMockPop({
1308
+ // strings: { name: "value" },
1309
+ // integers: { amount: 5 },
1310
+ });
1311
+ await command.do(pop);
1312
+ expect(replies.length).toBeGreaterThan(0);
1313
+ });
1314
+ });
1315
+ `;
1316
+ }
1317
+ await mkdir2(dirname(testPath), { recursive: true });
1318
+ await Bun.write(testPath, content);
1319
+ console.log(`✅ Created test: ${testPath}`);
1320
+ } else {
1321
+ console.log(`Unknown generator type: ${type}`);
1322
+ }
1323
+ }
1324
+ async function runDoctor(args) {
1325
+ const fs = __require("node:fs");
1326
+ const path = __require("node:path");
1327
+ console.log(`
1328
+ \uD83E\uDE7A Popii Doctor — Checking your environment
1329
+ `);
1330
+ let allOk = true;
1331
+ const check = (label, ok, hint) => {
1332
+ console.log(` ${ok ? "✅" : "❌"} ${label}`);
1333
+ if (!ok && hint)
1334
+ console.log(` → ${hint}`);
1335
+ if (!ok)
1336
+ allOk = false;
1337
+ };
1338
+ const envPath = path.join(process.cwd(), ".env");
1339
+ const hasEnv = fs.existsSync(envPath);
1340
+ check(".env file exists", hasEnv, "Create a .env file — run: bunx popii init");
1341
+ let envVars = {};
1342
+ if (hasEnv) {
1343
+ for (const line of fs.readFileSync(envPath, "utf8").split(`
1344
+ `)) {
1345
+ const eqIdx = line.indexOf("=");
1346
+ if (eqIdx > 0)
1347
+ envVars[line.slice(0, eqIdx).trim()] = line.slice(eqIdx + 1).trim();
1348
+ }
1349
+ }
1350
+ const token = process.env.DISCORD_TOKEN || envVars["DISCORD_TOKEN"];
1351
+ check("DISCORD_TOKEN is set", !!token, "Add DISCORD_TOKEN=<your-token> to your .env file");
1352
+ if (token) {
1353
+ const sp = spinner();
1354
+ sp.start("Validating Discord token...");
1355
+ try {
1356
+ const res = await fetch("https://discord.com/api/v10/users/@me", {
1357
+ headers: { Authorization: `Bot ${token}` }
1358
+ });
1359
+ sp.stop("");
1360
+ if (res.ok) {
1361
+ const data = await res.json();
1362
+ check(`Token is valid (bot: ${data.username})`, true);
1363
+ } else {
1364
+ check("Token is valid", false, `Discord API returned ${res.status} — verify the token in the Developer Portal`);
1365
+ }
1366
+ } catch {
1367
+ sp.stop("");
1368
+ check("Discord API is reachable", false, "Could not reach discord.com — check your internet connection");
1369
+ }
1370
+ }
1371
+ const entryFile = args[1] || path.join("src", "index.ts");
1372
+ check(`Entry file exists (${entryFile})`, fs.existsSync(path.join(process.cwd(), entryFile)), `Create ${entryFile} or specify a path: popii doctor <entry>`);
1373
+ for (const dir of ["src/commands", "src/events", "src/snaps", "src/middlewares", "src/tasks"]) {
1374
+ if (!fs.existsSync(path.join(process.cwd(), dir))) {
1375
+ check(`Directory ${dir} exists`, false, `Create it: mkdir -p ${dir}`);
1376
+ }
1377
+ }
1378
+ const pkgPath = path.join(process.cwd(), "package.json");
1379
+ if (fs.existsSync(pkgPath)) {
1380
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
1381
+ check("popii is in package.json", !!(pkg.dependencies?.popii || pkg.devDependencies?.popii), "Run: bun add popii");
1382
+ } else {
1383
+ check("package.json exists", false, "Run: bunx popii init");
1384
+ }
1385
+ const bunVersion = process.versions.bun;
1386
+ if (bunVersion) {
1387
+ const [major = 0, minor = 0] = bunVersion.split(".").map(Number);
1388
+ check(`Bun >= 1.1.0 (found ${bunVersion})`, major > 1 || major === 1 && minor >= 1, "Upgrade: bun upgrade");
1389
+ }
1390
+ console.log();
1391
+ if (allOk) {
1392
+ console.log(`✨ All checks passed. Your bot is ready to run.
1393
+ `);
1394
+ } else {
1395
+ console.log(`⚠️ Some checks failed. Fix the issues above and re-run popii doctor.
1396
+ `);
1397
+ process.exit(1);
1398
+ }
1399
+ }
1400
+ async function runDeploy(args) {
1401
+ const fs = __require("node:fs");
1402
+ const path = __require("node:path");
1403
+ let target = args[1];
1404
+ if (!target) {
1405
+ const choice = await select({
1406
+ message: "Choose a deployment target:",
1407
+ options: [
1408
+ { value: "railway", label: "Railway", hint: "generates railway.json" },
1409
+ { value: "fly", label: "Fly.io", hint: "generates fly.toml" },
1410
+ { value: "vps", label: "VPS / systemd", hint: "generates a systemd service unit" }
1411
+ ]
1412
+ });
1413
+ if (isCancel(choice))
1414
+ process.exit(0);
1415
+ target = choice;
1416
+ }
1417
+ let pkg = {};
1418
+ try {
1419
+ pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
1420
+ } catch {}
1421
+ const appName = pkg.name ?? "popii-bot";
1422
+ if (target === "railway") {
1423
+ const config = {
1424
+ $schema: "https://railway.app/railway.schema.json",
1425
+ build: { builder: "DOCKERFILE" },
1426
+ deploy: { restartPolicyType: "ON_FAILURE", restartPolicyMaxRetries: 10 }
1427
+ };
1428
+ await Bun.write(path.join(process.cwd(), "railway.json"), JSON.stringify(config, null, 2) + `
1429
+ `);
1430
+ console.log(`✅ Created railway.json
1431
+ `);
1432
+ console.log("Next steps:");
1433
+ console.log(" 1. npm i -g @railway/cli && railway login");
1434
+ console.log(" 2. railway link (or railway init)");
1435
+ console.log(" 3. railway variables set DISCORD_TOKEN=xxx");
1436
+ console.log(` 4. railway up
1437
+ `);
1438
+ } else if (target === "fly") {
1439
+ const toml = `# Generated by popii deploy
1440
+ app = "${appName}"
1441
+ primary_region = "iad"
1442
+
1443
+ [build]
1444
+ dockerfile = "Dockerfile"
1445
+
1446
+ [env]
1447
+ PORT = "3000"
1448
+ `;
1449
+ await Bun.write(path.join(process.cwd(), "fly.toml"), toml);
1450
+ console.log(`✅ Created fly.toml
1451
+ `);
1452
+ console.log("Next steps:");
1453
+ console.log(" 1. curl -L https://fly.io/install.sh | sh && fly auth login");
1454
+ console.log(" 2. fly launch --no-deploy");
1455
+ console.log(" 3. fly secrets set DISCORD_TOKEN=xxx");
1456
+ console.log(` 4. fly deploy
1457
+ `);
1458
+ } else if (target === "vps") {
1459
+ const service = `[Unit]
1460
+ Description=${appName} Discord Bot
1461
+ After=network.target
1462
+
1463
+ [Service]
1464
+ Type=simple
1465
+ User=popii
1466
+ WorkingDirectory=/opt/${appName}
1467
+ ExecStart=/usr/local/bin/bun run start
1468
+ EnvironmentFile=/opt/${appName}/.env
1469
+ Restart=always
1470
+ RestartSec=10
1471
+ StandardOutput=journal
1472
+ StandardError=journal
1473
+
1474
+ [Install]
1475
+ WantedBy=multi-user.target
1476
+ `;
1477
+ await Bun.write(path.join(process.cwd(), `${appName}.service`), service);
1478
+ console.log(`✅ Created ${appName}.service
1479
+ `);
1480
+ console.log("Next steps:");
1481
+ console.log(` 1. scp -r . user@host:/opt/${appName}`);
1482
+ console.log(` 2. On the VPS: curl -fsSL https://bun.sh/install | bash`);
1483
+ console.log(` 3. sudo cp ${appName}.service /etc/systemd/system/`);
1484
+ console.log(` sudo systemctl daemon-reload`);
1485
+ console.log(` sudo systemctl enable --now ${appName}`);
1486
+ console.log(` 4. journalctl -u ${appName} -f
1487
+ `);
1488
+ } else {
1489
+ console.log(`Unknown target: ${target}. Valid options: railway, fly, vps`);
1490
+ process.exit(1);
1491
+ }
1492
+ }
1493
+ async function runDashboard(args) {
1494
+ const fs = __require("node:fs");
1495
+ const path = __require("node:path");
1496
+ let envVars = {};
1497
+ const envPath = path.join(process.cwd(), ".env");
1498
+ if (fs.existsSync(envPath)) {
1499
+ for (const line of fs.readFileSync(envPath, "utf8").split(`
1500
+ `)) {
1501
+ const eqIdx = line.indexOf("=");
1502
+ if (eqIdx > 0)
1503
+ envVars[line.slice(0, eqIdx).trim()] = line.slice(eqIdx + 1).trim();
1504
+ }
1505
+ }
1506
+ const clientId = process.env.DISCORD_CLIENT_ID || envVars["DISCORD_CLIENT_ID"];
1507
+ const port = args[1] ? parseInt(args[1]) : 3000;
1508
+ const dashPath = args[2] ?? "/dashboard";
1509
+ const redirectUri = `http://localhost:${port}${dashPath}/callback`;
1510
+ console.log(`
1511
+ \uD83D\uDCCB Dashboard Setup
1512
+ `);
1513
+ console.log("1. Discord Developer Portal → Your App → OAuth2 → Redirects");
1514
+ console.log(` Add: ${redirectUri}`);
1515
+ console.log(` (Replace localhost with your domain in production)
1516
+ `);
1517
+ console.log("2. Required .env variables:");
1518
+ console.log(` DISCORD_CLIENT_ID = <OAuth2 General → Client ID>`);
1519
+ console.log(` DISCORD_CLIENT_SECRET = <OAuth2 General → Client Secret>`);
1520
+ console.log(` REDIRECT_URI = ${redirectUri}
1521
+ `);
1522
+ if (clientId) {
1523
+ const scopes = "bot%20applications.commands";
1524
+ console.log("3. Bot invite URL (Administrator):");
1525
+ console.log(` https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=8&scope=${scopes}
1526
+ `);
1527
+ const oauthScopes = "identify%20guilds";
1528
+ console.log("4. Dashboard login URL (test in browser):");
1529
+ console.log(` https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${oauthScopes}
1530
+ `);
1531
+ } else {
1532
+ console.log(`3. Set DISCORD_CLIENT_ID in .env to generate invite and login URLs.
1533
+ `);
1534
+ }
1535
+ console.log("5. webPlugin config:");
1536
+ console.log(` webPlugin({`);
1537
+ console.log(` port: ${port},`);
1538
+ console.log(` dashboard: true,`);
1539
+ console.log(` oauth2: {`);
1540
+ console.log(` clientId: process.env.DISCORD_CLIENT_ID!,`);
1541
+ console.log(` clientSecret: process.env.DISCORD_CLIENT_SECRET!,`);
1542
+ console.log(` redirectUri: process.env.REDIRECT_URI!,`);
1543
+ console.log(` },`);
1544
+ console.log(` })
1545
+ `);
1546
+ console.log("Tip: pass a custom port and path: popii dashboard <port> <dashPath>");
1547
+ console.log(` e.g. popii dashboard 6767 /dashboard
1548
+ `);
1549
+ }
1550
+ async function runInit(args) {
1551
+ if (!args.includes("--no-browser") && process.stdout.isTTY) {
1552
+ await initViaWeb();
1553
+ return;
1554
+ }
1555
+ intro("Welcome to Popii loader! Let's set up your new bot.");
1556
+ const botName = await text({
1557
+ message: "What is the name of your bot?",
1558
+ placeholder: "my-popii-bot",
1559
+ validate(value) {
1560
+ if (!value || value.trim().length === 0)
1561
+ return "Name is required!";
1562
+ if (value.trim().toLowerCase() === "popii")
1563
+ return "Your bot cannot be named 'popii' as it causes a dependency loop!";
1564
+ }
1565
+ });
1566
+ if (isCancel(botName))
1567
+ process.exit(1);
1568
+ if (botName === "popii") {
1569
+ console.error("Your bot cannot be named 'popii' as it causes a dependency loop!");
1570
+ process.exit(1);
1571
+ }
1572
+ const selectedPlugins = await multiselect({
1573
+ message: "Which built-in plugins would you like to enable?",
1574
+ options: [
1575
+ { value: "uiPlugin", label: "UI Plugin", hint: ".paginate(), .prompt(), .form()" },
1576
+ { value: "sqlitePlugin", label: "SQLite", hint: "Bun SQLite database in context" },
1577
+ { value: "errorHandlerPlugin", label: "Error Handler", hint: "Catches PopiiErrors gracefully" },
1578
+ { value: "commandLoggerPlugin", label: "Command Logger", hint: "Logs command executions" },
1579
+ { value: "commandAnalyticPlugin", label: "Analytics", hint: "Tracks command usage stats" },
1580
+ { value: "permissionGuardPlugin", label: "Permission Guard", hint: "Enforces permissions automatically" },
1581
+ { value: "reloadPlugin", label: "Hot Reload", hint: "Adds a /reload command" },
1582
+ { value: "webPlugin", label: "Web Server", hint: "/health and /metrics endpoints" },
1583
+ { value: "telemetryPlugin", label: "Telemetry", hint: "Performance monitoring" },
1584
+ { value: "voicePlugin", label: "Voice", hint: "Voice channel support" },
1585
+ { value: "mongoosePlugin", label: "Mongoose", hint: "MongoDB via Mongoose ODM" },
1586
+ { value: "deskPlugin", label: "Desk", hint: "Support ticket system" },
1587
+ { value: "popiiAiPlugin", label: "AI", hint: "Claude AI integration" },
1588
+ { value: "payPlugin", label: "Pay", hint: "Payment processing" },
1589
+ { value: "autoModPlugin", label: "AutoMod", hint: "Automated moderation" },
1590
+ { value: "economyPlugin", label: "Economy", hint: "Virtual currency system" },
1591
+ { value: "giveawayPlugin", label: "Giveaway", hint: "Run giveaways in channels" },
1592
+ { value: "activityRotatorPlugin", label: "Activity Rotator", hint: "Rotates bot status messages" },
1593
+ { value: "captchaPlugin", label: "Captcha", hint: "Verification captchas" },
1594
+ { value: "canvasPlugin", label: "Canvas", hint: "Image generation" },
1595
+ { value: "lastFmPlugin", label: "Last.fm", hint: "Last.fm music integration" }
1596
+ ],
1597
+ required: false
1598
+ });
1599
+ if (isCancel(selectedPlugins))
1600
+ process.exit(1);
1601
+ const botToken = await text({
1602
+ message: "Discord bot token (Leave blank to add later)",
1603
+ placeholder: "MTAx..."
1604
+ });
1605
+ if (isCancel(botToken))
1606
+ process.exit(1);
1607
+ const installDeps = await confirm({
1608
+ message: "Would you like to install dependencies via Bun now?",
1609
+ initialValue: true
1610
+ });
1611
+ if (isCancel(installDeps))
1612
+ process.exit(1);
1613
+ const genDocker = await confirm({
1614
+ message: "Would you like to generate Docker deployment files for production?",
1615
+ initialValue: false
1616
+ });
1617
+ if (isCancel(genDocker))
1618
+ process.exit(1);
1619
+ const s = spinner();
1620
+ s.start("Scaffolding your project...");
1621
+ await scaffoldProject({
1622
+ botName,
1623
+ plugins: selectedPlugins,
1624
+ botToken,
1625
+ installDeps,
1626
+ genDocker
1627
+ });
1628
+ s.stop("All done!");
1629
+ outro(`Your project is ready!
1630
+
1631
+ cd ${botName}
1632
+ bun run dev`);
1633
+ }
1634
+
1635
+ // src/cli.ts
1636
+ async function main() {
1637
+ const args = process.argv.slice(2);
1638
+ if (args[0] === "dev") {
1639
+ await runDev(args);
1640
+ return;
1641
+ }
1642
+ if (args[0] === "sync") {
1643
+ await runSync(args);
1644
+ return;
1645
+ }
1646
+ if (args[0] === "console") {
1647
+ await runConsole(args);
1648
+ return;
1649
+ }
1650
+ if (args[0] === "migrate") {
1651
+ await runMigrate(args);
1652
+ return;
1653
+ }
1654
+ if (args[0] === "install") {
1655
+ await runInstall(args);
1656
+ return;
1657
+ }
1658
+ if (args[0] === "add") {
1659
+ await runAdd(args);
1660
+ return;
1661
+ }
1662
+ if (args[0] === "remove") {
1663
+ await runRemove(args);
1664
+ return;
1665
+ }
1666
+ if (args[0] === "search") {
1667
+ await runSearch(args);
1668
+ return;
1669
+ }
1670
+ if (args[0] === "list") {
1671
+ await runList(args);
1672
+ return;
1673
+ }
1674
+ if (args[0] === "g" || args[0] === "generate") {
1675
+ await runGenerate(args);
1676
+ return;
1677
+ }
1678
+ if (args[0] === "doctor") {
1679
+ await runDoctor(args);
1680
+ return;
1681
+ }
1682
+ if (args[0] === "deploy") {
1683
+ await runDeploy(args);
1684
+ return;
1685
+ }
1686
+ if (args[0] === "dashboard") {
1687
+ await runDashboard(args);
1688
+ return;
1689
+ }
1690
+ if (args[0] !== "init") {
1691
+ console.log(`Usage:
1692
+ bunx popii init
1693
+ bunx popii dev
1694
+ bunx popii sync
1695
+ bunx popii console
1696
+ bunx popii doctor
1697
+ bunx popii deploy [railway|fly|vps]
1698
+ bunx popii dashboard [port] [path]
1699
+ bunx popii install <user/repo[@tag]>
1700
+ bunx popii migrate <create|up> [name]
1701
+ bunx popii g <command|event|snap|middleware|task|test> <name>
1702
+ bunx popii add <plugin-name>
1703
+ bunx popii remove <plugin-name>
1704
+ bunx popii search <query>
1705
+ bunx popii list`);
1706
+ process.exit(1);
1707
+ }
1708
+ await runInit(args);
1709
+ }
1710
+ main().catch(console.error);