pulse-rb 1.2.24 → 1.3.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.
package/dist/index.js CHANGED
@@ -5,11 +5,12 @@ var path = require('path');
5
5
  var pc = require('picocolors');
6
6
  var prompts = require('@clack/prompts');
7
7
  var fs = require('fs');
8
- var pathe = require('pathe');
9
- var ofetch = require('ofetch');
8
+ var typescriptToLua = require('typescript-to-lua');
10
9
  var child_process = require('child_process');
10
+ var pathe = require('pathe');
11
11
  var chokidar = require('chokidar');
12
12
  var execa = require('execa');
13
+ var ofetch = require('ofetch');
13
14
  var globby = require('globby');
14
15
  var promises = require('fs/promises');
15
16
  var citty = require('citty');
@@ -55,15 +56,15 @@ var RB_VERSION, RB_HOME, IRONBREW2_DIR, IRONBREW2_REPO, ALWAYS_EXCLUDE, VALID_UI
55
56
  var init_constants = __esm({
56
57
  "src/constants.ts"() {
57
58
  init_cjs_shims();
58
- RB_VERSION = "1.2.24";
59
+ RB_VERSION = "1.3.0";
59
60
  RB_HOME = path.join(os.homedir(), ".rb");
60
61
  path.join(RB_HOME, "bin");
61
62
  IRONBREW2_DIR = path.join(RB_HOME, "ironbrew2");
62
- IRONBREW2_REPO = "https://github.com/LorekeeperZinnia/IronBrew2.git";
63
+ IRONBREW2_REPO = "https://github.com/Trollicus/ironbrew-2.git";
63
64
  ALWAYS_EXCLUDE = /* @__PURE__ */ new Set(["types.lua"]);
64
65
  VALID_UI_LIBS = /* @__PURE__ */ new Set(["linoria", "windui"]);
65
66
  OBFUSCATION_PRESETS = ["Low", "Medium", "Strong"];
66
- CDN_BASE_URL = "https://cdn.pulse-rb.dev";
67
+ CDN_BASE_URL = "https://pub-2072661efb7e45a1ac7f2aea691b0627.r2.dev";
67
68
  }
68
69
  });
69
70
 
@@ -1093,129 +1094,6 @@ var init_transpiler = __esm({
1093
1094
  };
1094
1095
  }
1095
1096
  });
1096
-
1097
- // src/commands/publish.ts
1098
- var publish_exports = {};
1099
- __export(publish_exports, {
1100
- cmdPublish: () => cmdPublish,
1101
- hasCdnConfig: () => hasCdnConfig,
1102
- readPublishConfig: () => readPublishConfig
1103
- });
1104
- function readPublishConfig() {
1105
- for (const p of CONFIG_CANDIDATES) {
1106
- if (!fs.existsSync(p)) continue;
1107
- const config = {};
1108
- for (const line of fs.readFileSync(p, "utf8").split("\n")) {
1109
- const trimmed = line.trim();
1110
- if (!trimmed || trimmed.startsWith("#")) continue;
1111
- const eq = trimmed.indexOf("=");
1112
- if (eq < 0) continue;
1113
- const key = trimmed.slice(0, eq).trim();
1114
- config[key] = trimmed.slice(eq + 1).trim();
1115
- }
1116
- return config;
1117
- }
1118
- return {};
1119
- }
1120
- function hasCdnConfig(projectRoot) {
1121
- const candidates = projectRoot ? [pathe.join(projectRoot, ".rb-publish"), ...CONFIG_CANDIDATES] : CONFIG_CANDIDATES;
1122
- for (const p of candidates) {
1123
- if (!fs.existsSync(p)) continue;
1124
- const content = fs.readFileSync(p, "utf8");
1125
- if (content.includes("R2_ACCOUNT_ID") && content.includes("R2_SECRET_TOKEN") && content.includes("R2_PUBLIC_URL")) return true;
1126
- }
1127
- return false;
1128
- }
1129
- async function uploadFile(content, remotePath, config) {
1130
- const url = `https://api.cloudflare.com/client/v4/accounts/${config.R2_ACCOUNT_ID}/r2/buckets/${config.R2_BUCKET}/objects/${remotePath}`;
1131
- await ofetch.ofetch(url, {
1132
- method: "PUT",
1133
- body: content,
1134
- headers: {
1135
- Authorization: `Bearer ${config.R2_SECRET_TOKEN}`,
1136
- "Content-Type": remotePath.endsWith(".lua") ? "text/plain; charset=utf-8" : "application/octet-stream"
1137
- }
1138
- });
1139
- return `${config.R2_PUBLIC_URL}/${remotePath}`;
1140
- }
1141
- async function cmdPublish(_args) {
1142
- pHeader("publish");
1143
- const config = readPublishConfig();
1144
- const missing = ["R2_ACCOUNT_ID", "R2_SECRET_TOKEN", "R2_BUCKET", "R2_PUBLIC_URL"].filter((k) => !config[k]);
1145
- if (missing.length) {
1146
- pFail("R2 credentials not configured");
1147
- console.log();
1148
- pInfo(`Create ${bold(".rb-publish")} in your project root (or ~/.rb-publish):`);
1149
- console.log();
1150
- console.log(` ${dim("R2_ACCOUNT_ID")}=your_cloudflare_account_id`);
1151
- console.log(` ${dim("R2_SECRET_TOKEN")}=cfat_your_api_token`);
1152
- console.log(` ${dim("R2_BUCKET")}=pulse-runtime`);
1153
- console.log(` ${dim("R2_PUBLIC_URL")}=https://pub-xxxx.r2.dev`);
1154
- console.log();
1155
- pInfo(`Get your token at: ${cyan("dash.cloudflare.com \u2192 R2 \u2192 Manage API Tokens")}`);
1156
- process.exit(1);
1157
- }
1158
- const PULSE_DIR2 = pathe.join(__dirname, "..", "pulse");
1159
- const ADAPTERS = pathe.join(__dirname, "..", "adapters");
1160
- const VERSION_PATH = `v${RB_VERSION}`;
1161
- const runtimeFile = pathe.join(PULSE_DIR2, "runtime.lua");
1162
- const helpersDir = pathe.join(PULSE_DIR2, "helpers");
1163
- if (!fs.existsSync(runtimeFile)) {
1164
- pFail("pulse/runtime.lua not found");
1165
- process.exit(1);
1166
- }
1167
- const bundleParts = [
1168
- `-- Pulse v${RB_VERSION} bundle (runtime + helpers)`,
1169
- fs.readFileSync(runtimeFile, "utf8").trimEnd()
1170
- ];
1171
- if (fs.existsSync(helpersDir)) {
1172
- for (const f of fs.readdirSync(helpersDir).filter((n) => n.endsWith(".lua")).sort()) {
1173
- bundleParts.push(`-- ${f}`);
1174
- bundleParts.push(fs.readFileSync(pathe.join(helpersDir, f), "utf8").trimEnd());
1175
- }
1176
- }
1177
- const bundleContent = Buffer.from(bundleParts.join("\n") + "\n", "utf8");
1178
- const uploads = [
1179
- { remote: `${VERSION_PATH}/bundle.lua`, content: bundleContent }
1180
- ];
1181
- for (const adapter of ["windui.lua", "linoria.lua"]) {
1182
- const p = pathe.join(ADAPTERS, adapter);
1183
- if (fs.existsSync(p)) uploads.push({
1184
- remote: `${VERSION_PATH}/adapters/${adapter}`,
1185
- content: fs.readFileSync(p)
1186
- });
1187
- }
1188
- pSection(`Uploading to R2 ${gray("(v" + RB_VERSION + " \xB7 " + uploads.length + " files)")}`);
1189
- for (const { remote, content } of uploads) {
1190
- try {
1191
- await uploadFile(content, remote, config);
1192
- pOk(remote, `${content.length.toLocaleString()} bytes`);
1193
- } catch (e) {
1194
- pFail(`Failed: ${remote}`, String(e?.data?.message ?? e?.message ?? "").split("\n")[0]);
1195
- process.exit(1);
1196
- }
1197
- }
1198
- console.log();
1199
- pOk(`Published ${gray("v" + RB_VERSION)}`);
1200
- pInfo(`CDN base: ${cyan(config.R2_PUBLIC_URL + "/" + VERSION_PATH)}`);
1201
- console.log();
1202
- }
1203
- var CONFIG_CANDIDATES;
1204
- var init_publish = __esm({
1205
- "src/commands/publish.ts"() {
1206
- init_cjs_shims();
1207
- init_ui();
1208
- init_constants();
1209
- CONFIG_CANDIDATES = [
1210
- pathe.join(process.cwd(), ".rb-publish"),
1211
- pathe.join(process.env["USERPROFILE"] ?? process.env["HOME"] ?? "", ".rb-publish")
1212
- ];
1213
- }
1214
- });
1215
- function adapterPath(ui) {
1216
- if (!VALID_UI_LIBS.has(ui)) throw new Error(`Unknown UI library '${ui}'. Valid: ${[...VALID_UI_LIBS].sort().join(", ")}`);
1217
- return path.join(ADAPTERS_DIR, `${ui}.lua`);
1218
- }
1219
1097
  function rglob(dir, exts) {
1220
1098
  if (!fs.existsSync(dir)) return [];
1221
1099
  const results = [];
@@ -1232,7 +1110,7 @@ function rglob(dir, exts) {
1232
1110
  walk(dir);
1233
1111
  return results;
1234
1112
  }
1235
- var PULSE_DIR, PULSE_RUNTIME, PULSE_HELPERS, PULSE_DEV_DIR, PULSE_UI_DIR, ADAPTERS_DIR, REEXEC_GUARD, DESTROY_REGISTRATION, DEFAULTS_RUNNER, Compiler;
1113
+ var PULSE_DIR, PULSE_DEV_DIR, PULSE_UI_DIR, REEXEC_GUARD, DESTROY_REGISTRATION, DEFAULTS_RUNNER, Compiler;
1236
1114
  var init_compiler = __esm({
1237
1115
  "src/compiler.ts"() {
1238
1116
  init_cjs_shims();
@@ -1240,13 +1118,12 @@ var init_compiler = __esm({
1240
1118
  init_layout();
1241
1119
  init_ui();
1242
1120
  init_constants();
1243
- init_publish();
1244
1121
  PULSE_DIR = path.join(__dirname, "..", "pulse");
1245
- PULSE_RUNTIME = path.join(PULSE_DIR, "runtime.lua");
1246
- PULSE_HELPERS = path.join(PULSE_DIR, "helpers");
1122
+ path.join(PULSE_DIR, "runtime.lua");
1123
+ path.join(PULSE_DIR, "helpers");
1247
1124
  PULSE_DEV_DIR = path.join(PULSE_DIR, "dev");
1248
1125
  PULSE_UI_DIR = path.join(PULSE_DIR, "ui");
1249
- ADAPTERS_DIR = path.join(__dirname, "..", "adapters");
1126
+ path.join(__dirname, "..", "adapters");
1250
1127
  REEXEC_GUARD = `-- Stop any previous instance before this one starts.
1251
1128
  -- Kills old Heartbeat loops, connections, and Linoria window.
1252
1129
  if _G.__AOT_R_DESTROY then pcall(_G.__AOT_R_DESTROY) end
@@ -1301,6 +1178,12 @@ end)
1301
1178
  this.buildDir = path.join(root, "build");
1302
1179
  }
1303
1180
  getUiLibrary() {
1181
+ const layoutTs = path.join(this.srcDir, "layout.ts");
1182
+ if (fs.existsSync(layoutTs)) {
1183
+ const content = fs.readFileSync(layoutTs, "utf8");
1184
+ const m = content.match(/uiLibrary\s*:\s*['"](\w+)['"]/);
1185
+ if (m && VALID_UI_LIBS.has(m[1])) return m[1];
1186
+ }
1304
1187
  const layoutFile = path.join(this.srcDir, "ui", "layout.rblua");
1305
1188
  if (!fs.existsSync(layoutFile)) return "linoria";
1306
1189
  try {
@@ -1319,7 +1202,9 @@ end)
1319
1202
  const ui = this.resolveUi(opts.ui);
1320
1203
  const src = this.srcDir;
1321
1204
  const uiDir = path.join(src, "ui");
1205
+ const pagesDir = path.join(src, "pages");
1322
1206
  const devDir = path.join(src, "dev");
1207
+ const layoutTs = path.join(src, "layout.ts");
1323
1208
  const exclude = new Set(ALWAYS_EXCLUDE);
1324
1209
  if (opts.compat) {
1325
1210
  const layoutFile = path.join(src, "ui", "layout.rblua");
@@ -1342,10 +1227,11 @@ end)
1342
1227
  if (fs.existsSync(bootstrap)) first.push(bootstrap);
1343
1228
  if (fs.existsSync(remotes)) first.push(remotes);
1344
1229
  const allFiles = [
1345
- ...rglob(src, [".lua", ".rblua"])
1230
+ ...rglob(src, [".lua", ".rblua", ".ts"])
1346
1231
  ].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1347
1232
  for (const f of allFiles) {
1348
1233
  if (f.startsWith(devDir + "\\") || f.startsWith(devDir + "/")) continue;
1234
+ if (f === layoutTs) continue;
1349
1235
  const rel = path.relative(src, f).replace(/\\/g, "/");
1350
1236
  if (exclude.has(rel)) continue;
1351
1237
  if (f === bootstrap || f === remotes || f === startup) continue;
@@ -1354,14 +1240,17 @@ end)
1354
1240
  uiFiles.push(f);
1355
1241
  continue;
1356
1242
  }
1357
- path.join(f, "...");
1243
+ if (f.startsWith(pagesDir + "\\") || f.startsWith(pagesDir + "/")) {
1244
+ uiFiles.push(f);
1245
+ continue;
1246
+ }
1358
1247
  if (path.relative(src, f).split(/[/\\]/).length === 1) continue;
1359
1248
  middle.push(f);
1360
1249
  }
1361
1250
  if (opts.dev) {
1362
1251
  const fwDevUi = path.join(PULSE_DEV_DIR, "ui");
1363
1252
  if (fs.existsSync(fwDevUi)) {
1364
- for (const f of rglob(fwDevUi, [".lua", ".rblua"]).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
1253
+ for (const f of rglob(fwDevUi, [".lua", ".rblua", ".ts"]).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
1365
1254
  uiFiles.push(f);
1366
1255
  }
1367
1256
  }
@@ -1372,11 +1261,11 @@ end)
1372
1261
  });
1373
1262
  uiFiles.sort((a, b) => {
1374
1263
  const an = path.basename(a), bn = path.basename(b);
1375
- const ai = an === "layout.rblua" ? 0 : 1;
1376
- const bi = bn === "layout.rblua" ? 0 : 1;
1264
+ const ai = an === "layout.rblua" || an === "layout.ts" ? 0 : 1;
1265
+ const bi = bn === "layout.rblua" || bn === "layout.ts" ? 0 : 1;
1377
1266
  return ai !== bi ? ai - bi : an.toLowerCase().localeCompare(bn.toLowerCase());
1378
1267
  });
1379
- if (fs.existsSync(PULSE_UI_DIR)) {
1268
+ if (!opts.cdn && fs.existsSync(PULSE_UI_DIR)) {
1380
1269
  for (const f of rglob(PULSE_UI_DIR, [".lua", ".rblua"]).sort((a, b) => path.basename(a).toLowerCase().localeCompare(path.basename(b).toLowerCase()))) {
1381
1270
  const stem = path.basename(f, path.extname(f));
1382
1271
  if (stem.startsWith("windui_") && ui !== "windui") continue;
@@ -1390,44 +1279,31 @@ end)
1390
1279
  }
1391
1280
  compile(opts = {}) {
1392
1281
  const ui = this.resolveUi(opts.ui);
1393
- const order = this.getLoadOrder({ ...opts, ui });
1394
- const hasRblua = order.some((f) => f.endsWith(".rblua"));
1282
+ const order = this.getLoadOrder({ ...opts, ui, cdn: true });
1283
+ const hasTs = order.some((f) => f.endsWith(".rblua") || f.endsWith(".ts"));
1395
1284
  const srcUiDir = path.join(this.srcDir, "ui");
1396
1285
  const pulseDevUi = path.join(PULSE_DEV_DIR, "ui");
1397
- const useCdn = !opts.dev && hasCdnConfig(this.root);
1398
1286
  const parts = [];
1399
1287
  parts.push("-- [re-execution guard]\n");
1400
1288
  parts.push(REEXEC_GUARD);
1401
1289
  parts.push("\n");
1402
- if (opts.dev) {
1403
- parts.push("-- [dev flag]\n");
1404
- parts.push("local _PULSE_DEV = true\n\n");
1405
- }
1406
- if (hasRblua) {
1407
- if (useCdn) {
1408
- const base = `${CDN_BASE_URL}/v${RB_VERSION}`;
1409
- parts.push(`-- Pulse v${RB_VERSION}
1290
+ if (hasTs) {
1291
+ const base = `${CDN_BASE_URL}/v${RB_VERSION}`;
1292
+ parts.push(`-- Pulse v${RB_VERSION}
1410
1293
  `);
1411
- parts.push(`local _P=loadstring(game:HttpGet("${base}/bundle.lua"))()
1294
+ parts.push(`local _P=loadstring(game:HttpGet("${base}/bundle.lua"))()
1412
1295
  `);
1413
- parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))()
1296
+ parts.push(`for _k,_v in pairs(_P) do _G[_k]=_v end
1414
1297
  `);
1415
- parts.push("\n");
1416
- } else {
1417
- if (fs.existsSync(PULSE_RUNTIME)) {
1418
- parts.push("-- [pulse/runtime.lua]\n");
1419
- parts.push(fs.readFileSync(PULSE_RUNTIME, "utf8"));
1420
- parts.push("\n");
1421
- if (fs.existsSync(PULSE_HELPERS)) {
1422
- for (const helper of fs.readdirSync(PULSE_HELPERS).filter((f) => f.endsWith(".lua")).sort()) {
1423
- parts.push(`-- [pulse/helpers/${helper}]
1298
+ parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))()
1424
1299
  `);
1425
- parts.push(fs.readFileSync(path.join(PULSE_HELPERS, helper), "utf8"));
1426
- parts.push("\n");
1427
- }
1428
- }
1429
- }
1430
- }
1300
+ parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
1301
+ `);
1302
+ parts.push(`local toggle,slider,dropdown,multidropdown=_P.toggle,_P.slider,_P.dropdown,_P.multidropdown
1303
+ `);
1304
+ parts.push(`local button,keybind,label,separator,groupbox,definePage=_P.button,_P.keybind,_P.label,_P.separator,_P.groupbox,_P.definePage
1305
+ `);
1306
+ parts.push("\n");
1431
1307
  }
1432
1308
  if (opts.dev && fs.existsSync(PULSE_DEV_DIR)) {
1433
1309
  const rbRoot = path.join(__dirname, "..", "..");
@@ -1440,13 +1316,6 @@ end)
1440
1316
  parts.push("\n");
1441
1317
  }
1442
1318
  }
1443
- const adapter = adapterPath(ui);
1444
- if (!useCdn && fs.existsSync(srcUiDir) && fs.existsSync(adapter)) {
1445
- parts.push(`-- [adapters/${ui}.lua]
1446
- `);
1447
- parts.push(fs.readFileSync(adapter, "utf8"));
1448
- parts.push("\n");
1449
- }
1450
1319
  const label = (p) => {
1451
1320
  try {
1452
1321
  return path.relative(this.root, p).replace(/\\/g, "/");
@@ -1490,12 +1359,31 @@ end)
1490
1359
  if (e instanceof TranspileError) throw new Error(`Transpile error in ${lbl}: ${e.message}`);
1491
1360
  throw e;
1492
1361
  }
1362
+ } else if (f.endsWith(".ts")) {
1363
+ const source = fs.readFileSync(f, "utf8");
1364
+ try {
1365
+ const result = typescriptToLua.transpileString(source, {
1366
+ luaTarget: "Lua53",
1367
+ noImplicitSelf: true,
1368
+ noHeader: true,
1369
+ noCheck: true
1370
+ });
1371
+ if (result.diagnostics && result.diagnostics.length) {
1372
+ const diag = result.diagnostics[0];
1373
+ const msg = typeof diag.messageText === "string" ? diag.messageText : diag.messageText.messageText ?? String(diag.messageText);
1374
+ throw new Error(msg);
1375
+ }
1376
+ parts.push(result.file?.lua ?? "");
1377
+ } catch (e) {
1378
+ if (e instanceof TranspileError) throw new Error(`Transpile error in ${lbl}: ${e.message}`);
1379
+ throw new Error(`TypeScript error in ${lbl}: ${e.message}`);
1380
+ }
1493
1381
  } else {
1494
1382
  parts.push(fs.readFileSync(f, "utf8"));
1495
1383
  }
1496
1384
  parts.push("\n");
1497
1385
  }
1498
- if (order.some((f) => f.endsWith(".rblua"))) {
1386
+ if (order.some((f) => f.endsWith(".rblua") || f.endsWith(".ts"))) {
1499
1387
  parts.push("-- [generated: defaults runner]\n");
1500
1388
  parts.push(DEFAULTS_RUNNER);
1501
1389
  parts.push("\n");
@@ -1603,8 +1491,12 @@ var init_checker = __esm({
1603
1491
  }
1604
1492
  });
1605
1493
  function ensureIronbrew2() {
1606
- const entry = path.join(IRONBREW2_DIR, "IronBrew2.lua");
1607
- if (fs.existsSync(entry)) return IRONBREW2_DIR;
1494
+ const exeDir = path.join(IRONBREW2_DIR, IB2_EXE_SUBDIR);
1495
+ const exePath = path.join(exeDir, IB2_EXE_NAME);
1496
+ if (fs.existsSync(exePath)) return IRONBREW2_DIR;
1497
+ if (fs.existsSync(IRONBREW2_DIR)) {
1498
+ fs.rmSync(IRONBREW2_DIR, { recursive: true, force: true });
1499
+ }
1608
1500
  pInfo("Downloading Ironbrew2 from GitHub...");
1609
1501
  fs.mkdirSync(path.dirname(IRONBREW2_DIR), { recursive: true });
1610
1502
  const result = child_process.spawnSync("git", ["clone", "--depth=1", IRONBREW2_REPO, IRONBREW2_DIR], {
@@ -1618,30 +1510,29 @@ function ensureIronbrew2() {
1618
1510
  return IRONBREW2_DIR;
1619
1511
  }
1620
1512
  async function obfuscateSource(source, ib2Dir) {
1621
- const { LuaFactory } = await import('wasmoon');
1622
- const factory = new LuaFactory();
1623
- const lua = await factory.createEngine();
1513
+ const exeDir = path.join(ib2Dir, IB2_EXE_SUBDIR);
1514
+ const exePath = path.join(exeDir, IB2_EXE_NAME);
1515
+ const outPath = path.join(exeDir, "out.lua");
1624
1516
  const tmpIn = path.join(os.tmpdir(), `rb_in_${Date.now()}.lua`);
1625
- const tmpOut = path.join(os.tmpdir(), `rb_out_${Date.now()}.lua`);
1626
1517
  try {
1627
1518
  fs.writeFileSync(tmpIn, source, "utf8");
1628
- const inPath = tmpIn.replace(/\\/g, "/");
1629
- const outPath = tmpOut.replace(/\\/g, "/");
1630
- await lua.doString(`arg = {[0]="IronBrew2.lua", [1]="${inPath}", [2]="${outPath}"}`);
1631
- const ib2Source = fs.readFileSync(path.join(ib2Dir, "IronBrew2.lua"), "utf8");
1632
- await lua.doString(ib2Source);
1633
- if (!fs.existsSync(tmpOut)) throw new Error("Ironbrew2 produced no output");
1634
- return fs.readFileSync(tmpOut, "utf8");
1519
+ const dllPath = path.join(exeDir, "IronBrew2 CLI.dll");
1520
+ const [cmd, args] = fs.existsSync(dllPath) ? ["dotnet", ["--roll-forward", "Major", dllPath, tmpIn]] : [exePath, [tmpIn]];
1521
+ const result = child_process.spawnSync(cmd, args, {
1522
+ cwd: exeDir,
1523
+ stdio: "pipe"
1524
+ });
1525
+ const stdout = result.stdout?.toString().trim() ?? "";
1526
+ const stderr = result.stderr?.toString().trim() ?? "";
1527
+ if (stdout.startsWith("ERR:")) throw new Error(stdout.slice(4).trim());
1528
+ if (result.status !== 0) throw new Error(stderr || stdout || `exit ${result.status}`);
1529
+ if (!fs.existsSync(outPath)) throw new Error("IronBrew2 CLI produced no output file");
1530
+ return fs.readFileSync(outPath, "utf8");
1635
1531
  } finally {
1636
- lua.global.close();
1637
1532
  try {
1638
1533
  __require("fs").unlinkSync(tmpIn);
1639
1534
  } catch {
1640
1535
  }
1641
- try {
1642
- __require("fs").unlinkSync(tmpOut);
1643
- } catch {
1644
- }
1645
1536
  }
1646
1537
  }
1647
1538
  async function obfuscatePipeline(builds, root) {
@@ -1660,7 +1551,8 @@ async function obfuscatePipeline(builds, root) {
1660
1551
  outputs.push(outPath);
1661
1552
  } catch (e) {
1662
1553
  pFail(`Ironbrew2 failed on ${__require("path").basename(source)}`);
1663
- pWarn(String(e?.message ?? e));
1554
+ const { pWarn: pWarn7 } = (init_ui(), __toCommonJS(ui_exports));
1555
+ pWarn7(String(e?.message ?? e));
1664
1556
  failed = true;
1665
1557
  }
1666
1558
  }
@@ -1677,13 +1569,17 @@ function copyToClipboard(content, label = "") {
1677
1569
  }
1678
1570
  } catch {
1679
1571
  }
1680
- pWarn("Could not find clipboard tool (clip.exe / pbcopy / xclip)");
1572
+ const { pWarn: pWarn7 } = (init_ui(), __toCommonJS(ui_exports));
1573
+ pWarn7("Could not find clipboard tool (clip.exe / pbcopy / xclip)");
1681
1574
  }
1575
+ var IB2_EXE_SUBDIR, IB2_EXE_NAME;
1682
1576
  var init_obfuscator = __esm({
1683
1577
  "src/obfuscator.ts"() {
1684
1578
  init_cjs_shims();
1685
1579
  init_ui();
1686
1580
  init_constants();
1581
+ IB2_EXE_SUBDIR = path.join("IronBrew2 CLI", "bin", "Debug", "netcoreapp3.1");
1582
+ IB2_EXE_NAME = "IronBrew2 CLI.exe";
1687
1583
  }
1688
1584
  });
1689
1585
 
@@ -1902,7 +1798,7 @@ function makeClaudeMd(name) {
1902
1798
  function makeAgentsMd(name) {
1903
1799
  return read("AGENTS.md").replace(/{NAME}/g, name);
1904
1800
  }
1905
- var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES, TEMPLATE_GITIGNORE, TEMPLATE_LAYOUT, TEMPLATE_PAGE_HOME, TEMPLATE_EX_SPEED, TEMPLATE_EX_FOV, TEMPLATE_EX_ESP, TEMPLATE_DEPLOY_EXAMPLE, MODULE_BOILERPLATE, MODULE_RBLUA_BOILERPLATE, REMOTE_BOILERPLATE;
1801
+ var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES, TEMPLATE_GITIGNORE, TEMPLATE_DEPLOY_EXAMPLE, MODULE_BOILERPLATE, MODULE_RBLUA_BOILERPLATE, REMOTE_BOILERPLATE, TEMPLATE_LAYOUT_TS, TEMPLATE_PAGE_HOME_TS, TEMPLATE_EX_SPEED_TS, TEMPLATE_EX_FOV_TS, TEMPLATE_EX_ESP_TS;
1906
1802
  var init_templates = __esm({
1907
1803
  "src/templates.ts"() {
1908
1804
  init_cjs_shims();
@@ -1910,15 +1806,20 @@ var init_templates = __esm({
1910
1806
  TEMPLATE_GLOBALS = read("globals.lua");
1911
1807
  TEMPLATE_REMOTES = read("remotes.lua");
1912
1808
  TEMPLATE_GITIGNORE = read("gitignore");
1913
- TEMPLATE_LAYOUT = read("layout.rblua");
1914
- TEMPLATE_PAGE_HOME = read("page_home.rblua");
1915
- TEMPLATE_EX_SPEED = read("example_speed.rblua");
1916
- TEMPLATE_EX_FOV = read("example_fov.rblua");
1917
- TEMPLATE_EX_ESP = read("example_esp.rblua");
1809
+ read("layout.rblua");
1810
+ read("page_home.rblua");
1811
+ read("example_speed.rblua");
1812
+ read("example_fov.rblua");
1813
+ read("example_esp.rblua");
1918
1814
  TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
1919
1815
  MODULE_BOILERPLATE = read("module.lua");
1920
1816
  MODULE_RBLUA_BOILERPLATE = read("module.rblua");
1921
1817
  REMOTE_BOILERPLATE = read("remote.lua");
1818
+ TEMPLATE_LAYOUT_TS = read("layout.ts");
1819
+ TEMPLATE_PAGE_HOME_TS = read("page_home.ts");
1820
+ TEMPLATE_EX_SPEED_TS = read("component_speed.ts");
1821
+ TEMPLATE_EX_FOV_TS = read("component_fov.ts");
1822
+ TEMPLATE_EX_ESP_TS = read("component_esp.ts");
1922
1823
  }
1923
1824
  });
1924
1825
 
@@ -1995,22 +1896,23 @@ async function cmdInit(args) {
1995
1896
  pHeader("init");
1996
1897
  pSection(`Scaffolding ${bold(name + "/")}`);
1997
1898
  for (const dir of [
1998
- "src/misc/helpers",
1999
- "src/player",
1899
+ "src/misc",
1900
+ "src/pages",
1901
+ "src/combat",
2000
1902
  "src/visuals",
2001
- "src/ui/pages",
2002
1903
  "build"
2003
1904
  ]) {
2004
1905
  fs.mkdirSync(pathe.join(dest, dir), { recursive: true });
2005
1906
  }
2006
1907
  const files = [
2007
- ["src/misc/helpers/globals.lua", TEMPLATE_GLOBALS.replace(/{NAME}/g, name)],
1908
+ ["src/misc/globals.lua", TEMPLATE_GLOBALS.replace(/{NAME}/g, name)],
2008
1909
  ["src/misc/remotes.lua", TEMPLATE_REMOTES],
2009
- ["src/ui/layout.rblua", TEMPLATE_LAYOUT.replace(/{NAME}/g, name)],
2010
- ["src/ui/pages/1_Home.rblua", TEMPLATE_PAGE_HOME],
2011
- ["src/player/SpeedHack.rblua", TEMPLATE_EX_SPEED],
2012
- ["src/player/FOVChanger.rblua", TEMPLATE_EX_FOV],
2013
- ["src/visuals/PlayerESP.rblua", TEMPLATE_EX_ESP],
1910
+ ["src/layout.ts", TEMPLATE_LAYOUT_TS.replace(/{NAME}/g, name)],
1911
+ ["src/pages/1_Home.ts", TEMPLATE_PAGE_HOME_TS],
1912
+ ["src/combat/SpeedHack.ts", TEMPLATE_EX_SPEED_TS],
1913
+ ["src/combat/FOVChanger.ts", TEMPLATE_EX_FOV_TS],
1914
+ ["src/visuals/PlayerESP.ts", TEMPLATE_EX_ESP_TS],
1915
+ ["tsconfig.json", TSCONFIG],
2014
1916
  [".rb-deploy.example", TEMPLATE_DEPLOY_EXAMPLE],
2015
1917
  [".gitignore", TEMPLATE_GITIGNORE],
2016
1918
  ["CLAUDE.md", makeClaudeMd(name)],
@@ -2073,6 +1975,7 @@ async function cmdInit(args) {
2073
1975
  console.log(` ${cyan("\u2192")} cd ${name} && pnpm build`);
2074
1976
  console.log();
2075
1977
  }
1978
+ var TSCONFIG;
2076
1979
  var init_init = __esm({
2077
1980
  "src/commands/init.ts"() {
2078
1981
  init_cjs_shims();
@@ -2080,6 +1983,18 @@ var init_init = __esm({
2080
1983
  init_templates();
2081
1984
  init_docs();
2082
1985
  init_constants();
1986
+ TSCONFIG = JSON.stringify({
1987
+ compilerOptions: {
1988
+ strict: true,
1989
+ noEmit: true,
1990
+ skipLibCheck: true,
1991
+ target: "ESNext",
1992
+ moduleResolution: "bundler",
1993
+ types: ["@rb-pulse/core"]
1994
+ },
1995
+ include: ["src/**/*.ts"],
1996
+ exclude: ["node_modules"]
1997
+ }, null, 2) + "\n";
2083
1998
  }
2084
1999
  });
2085
2000
 
@@ -2172,8 +2087,8 @@ async function cmdRemove(args) {
2172
2087
  console.log();
2173
2088
  return;
2174
2089
  }
2175
- const { rmSync } = await import('fs');
2176
- rmSync(target);
2090
+ const { rmSync: rmSync2 } = await import('fs');
2091
+ rmSync2(target);
2177
2092
  pOk(`Removed ${gray("src/" + relPath)}`);
2178
2093
  console.log();
2179
2094
  }
@@ -2930,6 +2845,136 @@ var init_mcp = __esm({
2930
2845
  }
2931
2846
  });
2932
2847
 
2848
+ // src/commands/publish.ts
2849
+ var publish_exports = {};
2850
+ __export(publish_exports, {
2851
+ cmdPublish: () => cmdPublish,
2852
+ hasCdnConfig: () => hasCdnConfig,
2853
+ readPublishConfig: () => readPublishConfig
2854
+ });
2855
+ function readPublishConfig() {
2856
+ for (const p of CONFIG_CANDIDATES) {
2857
+ if (!fs.existsSync(p)) continue;
2858
+ const config = {};
2859
+ for (const line of fs.readFileSync(p, "utf8").split("\n")) {
2860
+ const trimmed = line.trim();
2861
+ if (!trimmed || trimmed.startsWith("#")) continue;
2862
+ const eq = trimmed.indexOf("=");
2863
+ if (eq < 0) continue;
2864
+ const key = trimmed.slice(0, eq).trim();
2865
+ config[key] = trimmed.slice(eq + 1).trim();
2866
+ }
2867
+ return config;
2868
+ }
2869
+ return {};
2870
+ }
2871
+ function hasCdnConfig(projectRoot) {
2872
+ const candidates = projectRoot ? [pathe.join(projectRoot, ".rb-publish"), ...CONFIG_CANDIDATES] : CONFIG_CANDIDATES;
2873
+ for (const p of candidates) {
2874
+ if (!fs.existsSync(p)) continue;
2875
+ const content = fs.readFileSync(p, "utf8");
2876
+ if (content.includes("R2_ACCOUNT_ID") && content.includes("R2_SECRET_TOKEN") && content.includes("R2_PUBLIC_URL")) return true;
2877
+ }
2878
+ return false;
2879
+ }
2880
+ async function uploadFile(content, remotePath, config) {
2881
+ const url = `https://api.cloudflare.com/client/v4/accounts/${config.R2_ACCOUNT_ID}/r2/buckets/${config.R2_BUCKET}/objects/${remotePath}`;
2882
+ await ofetch.ofetch(url, {
2883
+ method: "PUT",
2884
+ body: content,
2885
+ headers: {
2886
+ Authorization: `Bearer ${config.R2_SECRET_TOKEN}`,
2887
+ "Content-Type": remotePath.endsWith(".lua") ? "text/plain; charset=utf-8" : "application/octet-stream"
2888
+ }
2889
+ });
2890
+ return `${config.R2_PUBLIC_URL}/${remotePath}`;
2891
+ }
2892
+ async function cmdPublish(_args) {
2893
+ pHeader("publish");
2894
+ const config = readPublishConfig();
2895
+ const missing = ["R2_ACCOUNT_ID", "R2_SECRET_TOKEN", "R2_BUCKET", "R2_PUBLIC_URL"].filter((k) => !config[k]);
2896
+ if (missing.length) {
2897
+ pFail("R2 credentials not configured");
2898
+ console.log();
2899
+ pInfo(`Create ${bold(".rb-publish")} in your project root (or ~/.rb-publish):`);
2900
+ console.log();
2901
+ console.log(` ${dim("R2_ACCOUNT_ID")}=your_cloudflare_account_id`);
2902
+ console.log(` ${dim("R2_SECRET_TOKEN")}=cfat_your_api_token`);
2903
+ console.log(` ${dim("R2_BUCKET")}=pulse-runtime`);
2904
+ console.log(` ${dim("R2_PUBLIC_URL")}=https://pub-xxxx.r2.dev`);
2905
+ console.log();
2906
+ pInfo(`Get your token at: ${cyan("dash.cloudflare.com \u2192 R2 \u2192 Manage API Tokens")}`);
2907
+ process.exit(1);
2908
+ }
2909
+ const PULSE_DIR2 = pathe.join(__dirname, "..", "pulse");
2910
+ const ADAPTERS = pathe.join(__dirname, "..", "adapters");
2911
+ const VERSION_PATH = `v${RB_VERSION}`;
2912
+ const runtimeFile = pathe.join(PULSE_DIR2, "runtime.lua");
2913
+ const helpersDir = pathe.join(PULSE_DIR2, "helpers");
2914
+ if (!fs.existsSync(runtimeFile)) {
2915
+ pFail("pulse/runtime.lua not found");
2916
+ process.exit(1);
2917
+ }
2918
+ const bundleParts = [
2919
+ `-- Pulse v${RB_VERSION} bundle (runtime + helpers)`,
2920
+ fs.readFileSync(runtimeFile, "utf8").trimEnd()
2921
+ ];
2922
+ if (fs.existsSync(helpersDir)) {
2923
+ for (const f of fs.readdirSync(helpersDir).filter((n) => n.endsWith(".lua")).sort()) {
2924
+ bundleParts.push(`-- ${f}`);
2925
+ bundleParts.push(fs.readFileSync(pathe.join(helpersDir, f), "utf8").trimEnd());
2926
+ }
2927
+ }
2928
+ bundleParts.push(
2929
+ "return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages}"
2930
+ );
2931
+ const bundleContent = Buffer.from(bundleParts.join("\n") + "\n", "utf8");
2932
+ const uploads = [
2933
+ { remote: `${VERSION_PATH}/bundle.lua`, content: bundleContent }
2934
+ ];
2935
+ const pulseUiDir = pathe.join(PULSE_DIR2, "ui");
2936
+ for (const adapter of ["windui", "linoria"]) {
2937
+ const p = pathe.join(ADAPTERS, `${adapter}.lua`);
2938
+ if (!fs.existsSync(p)) continue;
2939
+ const parts = [fs.readFileSync(p, "utf8").trimEnd()];
2940
+ const settings = pathe.join(pulseUiDir, `${adapter}_settings.lua`);
2941
+ if (fs.existsSync(settings)) {
2942
+ parts.push(`-- ${adapter}_settings`);
2943
+ parts.push(fs.readFileSync(settings, "utf8").trimEnd());
2944
+ }
2945
+ uploads.push({
2946
+ remote: `${VERSION_PATH}/adapters/${adapter}.lua`,
2947
+ content: Buffer.from(parts.join("\n") + "\n", "utf8")
2948
+ });
2949
+ }
2950
+ pSection(`Uploading to R2 ${gray("(v" + RB_VERSION + " \xB7 " + uploads.length + " files)")}`);
2951
+ for (const { remote, content } of uploads) {
2952
+ try {
2953
+ await uploadFile(content, remote, config);
2954
+ pOk(remote, `${content.length.toLocaleString()} bytes`);
2955
+ } catch (e) {
2956
+ pFail(`Failed: ${remote}`, String(e?.data?.message ?? e?.message ?? "").split("\n")[0]);
2957
+ process.exit(1);
2958
+ }
2959
+ }
2960
+ console.log();
2961
+ pOk(`Published ${gray("v" + RB_VERSION)}`);
2962
+ pInfo(`CDN base: ${cyan(config.R2_PUBLIC_URL + "/" + VERSION_PATH)}`);
2963
+ console.log();
2964
+ }
2965
+ var CONFIG_CANDIDATES;
2966
+ var init_publish = __esm({
2967
+ "src/commands/publish.ts"() {
2968
+ init_cjs_shims();
2969
+ init_ui();
2970
+ init_constants();
2971
+ CONFIG_CANDIDATES = [
2972
+ pathe.join(process.cwd(), ".rb-publish"),
2973
+ pathe.join(process.env["USERPROFILE"] ?? process.env["HOME"] ?? "", ".rb-publish")
2974
+ ];
2975
+ }
2976
+ });
2977
+
2933
2978
  // src/commands/install.ts
2934
2979
  var install_exports = {};
2935
2980
  __export(install_exports, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-rb",
3
- "version": "1.2.24",
3
+ "version": "1.3.0",
4
4
  "description": "rb CLI — Pulse framework build tool for Roblox script projects",
5
5
  "bin": {
6
6
  "rb": "./bin/rb.js"
@@ -28,6 +28,7 @@
28
28
  "ofetch": "^1.3.4",
29
29
  "pathe": "^1.1.2",
30
30
  "picocolors": "^1.1.0",
31
+ "typescript-to-lua": "^1.30.0",
31
32
  "wasmoon": "^1.16.0",
32
33
  "zod": "^4.4.3"
33
34
  },
@@ -37,7 +38,7 @@
37
38
  "@types/node": "^22.0.0",
38
39
  "tsup": "^8.3.0",
39
40
  "tsx": "^4.19.0",
40
- "typescript": "^5.7.0",
41
+ "typescript": "^5.8.3",
41
42
  "vitest": "^2.1.0"
42
43
  },
43
44
  "engines": {
package/pulse/runtime.lua CHANGED
@@ -341,3 +341,157 @@ _PulseDestroy = _PulseDestroyAll
341
341
  -- Ordered list of { type, id, value } entries populated by transpiler-generated code.
342
342
  -- Consumed by the compiler-generated defaults runner at end of script.
343
343
  _PULSE_DEFAULTS = {}
344
+
345
+ -- ── TypeScript-compatible API ─────────────────────────────────────────────────
346
+ -- SolidJS-style signals + Next.js-style pages compiled from TypeScript via TSTL.
347
+ -- These globals are available in .ts source files without any imports.
348
+
349
+ local _currentComponent = nil
350
+ local _sigCounter = 0
351
+ _Pages = {}
352
+
353
+ local function _signal(default)
354
+ _sigCounter = _sigCounter + 1
355
+ local s = Signal(default)
356
+ s._id = ((_currentComponent and _currentComponent._name) or "g") .. "_s" .. _sigCounter
357
+ s.set = function(_, v) s:set(v) end
358
+ s.toggle = function(_) s:set(not s:get()) end
359
+ s.watch = function(_, fn) return s:onChange(fn) end
360
+ s.update = function(_, fn) s:set(fn(s:get())) end
361
+ return s
362
+ end
363
+
364
+ local function _computed(fn) return Computed(fn) end
365
+
366
+ local function _widgetBase(t)
367
+ function t:bind(sig)
368
+ self.signal = sig
369
+ self.id = sig and (sig._id or "?") or nil
370
+ return self
371
+ end
372
+ function t:withTip(text) self.tip = text; return self end
373
+ function t:withDefault(v) self.default = v; return self end
374
+ return t
375
+ end
376
+
377
+ local function _toggle(lbl, opts)
378
+ return _widgetBase({ type="toggle", label=lbl, tip=(opts and opts.tip) or "" })
379
+ end
380
+ local function _slider(lbl, opts)
381
+ return _widgetBase({ type="slider", label=lbl,
382
+ min=(opts and opts.min) or 0, max=(opts and opts.max) or 100,
383
+ tip=(opts and opts.tip) or "", suffix=(opts and opts.suffix) or "" })
384
+ end
385
+ local function _dropdown(lbl, opts)
386
+ return _widgetBase({ type="dropdown", label=lbl,
387
+ options=(opts and opts.options) or {}, tip=(opts and opts.tip) or "" })
388
+ end
389
+ local function _multidropdown(lbl, opts)
390
+ return _widgetBase({ type="multidropdown", label=lbl,
391
+ options=(opts and opts.options) or {}, tip=(opts and opts.tip) or "" })
392
+ end
393
+ local function _button(lbl, fnOrOpts)
394
+ local fn, tip
395
+ if type(fnOrOpts) == "function" then fn = fnOrOpts
396
+ elseif type(fnOrOpts) == "table" then tip = fnOrOpts.tip end
397
+ local t = { type="button", label=lbl, tip=tip or "", action=fn }
398
+ function t:onClick(cb) self.action = cb; return self end
399
+ function t:bind() return self end
400
+ return t
401
+ end
402
+ local function _keybind(lbl, opts)
403
+ return _widgetBase({ type="keybind", label=lbl,
404
+ key=(opts and opts.key) or "None", tip=(opts and opts.tip) or "" })
405
+ end
406
+ local function _label(text)
407
+ local t = { type="label", label=text }; function t:bind() return self end; return t
408
+ end
409
+ local function _separator()
410
+ local t = { type="separator" }; function t:bind() return self end; return t
411
+ end
412
+
413
+ local function _needComp(name)
414
+ assert(_currentComponent, "on." .. name .. " must be called inside defineComponent()")
415
+ return _currentComponent
416
+ end
417
+
418
+ local function _uid() return tostring(math.random(100000,999999)) end
419
+
420
+ local function _hbBind(svc, optsOrFn, fn)
421
+ local comp = _needComp("event")
422
+ local opts, cb
423
+ if type(optsOrFn) == "function" then cb=optsOrFn; opts={}
424
+ else opts=optsOrFn; cb=fn end
425
+ local when=opts.when; local every=opts.every; local last=0
426
+ comp:bind("ev_".._uid(), svc:Connect(function(a,b)
427
+ if when and not when() then return end
428
+ if every then
429
+ local t=type(every)=="table" and every() or every
430
+ local now=tick(); if (now-last)<t then return end; last=now
431
+ end
432
+ local ok,err=pcall(cb,a,b)
433
+ if not ok then warn("[Pulse] "..comp._name.." event error: "..tostring(err)) end
434
+ end))
435
+ end
436
+
437
+ local _on = {}
438
+ _on.heartbeat = function(a,b) _hbBind(_PulseRS.Heartbeat, a,b) end
439
+ _on.renderStepped = function(a,b) _hbBind(_PulseRS.RenderStepped, a,b) end
440
+ _on.stepped = function(a,b) _hbBind(_PulseRS.Stepped, a,b) end
441
+ _on.inputBegan = function(fn) local c=_needComp("inputBegan"); c:bind("ib_".._uid(), _PulseUIS.InputBegan:Connect(fn)) end
442
+ _on.inputEnded = function(fn) local c=_needComp("inputEnded"); c:bind("ie_".._uid(), _PulseUIS.InputEnded:Connect(fn)) end
443
+ _on.characterAdded = function(fn) local c=_needComp("characterAdded"); c:bind("ca_".._uid(), _LocalPlayer.CharacterAdded:Connect(fn)) end
444
+ _on.characterRemoving = function(fn) local c=_needComp("characterRemoving"); c:bind("cr_".._uid(), _LocalPlayer.CharacterRemoving:Connect(fn)) end
445
+ _on.respawn = function(fn) _needComp("respawn"):onRespawn(fn) end
446
+ _on.signal = function(sig,fn) _needComp("signal"):watch(sig,fn) end
447
+ _on.after = function(s,fn) _needComp("after"):task(s,fn) end
448
+
449
+ local function _defineComponent(name, setup)
450
+ local comp = Component(name)
451
+ _currentComponent = comp
452
+ local ok, widgets = pcall(setup)
453
+ _currentComponent = nil
454
+ if not ok then warn("[Pulse] defineComponent('"..name.."') error: "..tostring(widgets)); widgets={} end
455
+ widgets = widgets or {}
456
+ comp._ui = widgets
457
+ for _, w in ipairs(widgets) do
458
+ if w.default ~= nil and w.signal then
459
+ local kind = w.type == "toggle" and "toggle" or "option"
460
+ _PULSE_DEFAULTS[#_PULSE_DEFAULTS+1] = {
461
+ type=kind, id=w.id or (name.."_?"), value=w.default,
462
+ set=function(v) if w.signal then w.signal.set(nil,v) end end
463
+ }
464
+ end
465
+ end
466
+ return comp
467
+ end
468
+
469
+ local function _groupbox(side, title, opts)
470
+ return { type="groupbox", side=side, title=title,
471
+ icon=(opts and opts.icon) or "",
472
+ mount=(opts and opts.mount) or nil,
473
+ widgets=(opts and opts.widgets) or {} }
474
+ end
475
+
476
+ local function _definePage(title, opts, layoutFn)
477
+ local page = { title=title, icon=(opts and opts.icon) or "circle",
478
+ layout=layoutFn and layoutFn() or {} }
479
+ table.insert(_Pages, page)
480
+ return page
481
+ end
482
+
483
+ -- Expose as globals (available in TypeScript-compiled Lua without imports)
484
+ signal = _signal
485
+ computed = _computed
486
+ defineComponent = _defineComponent
487
+ on = _on
488
+ toggle = _toggle
489
+ slider = _slider
490
+ dropdown = _dropdown
491
+ multidropdown = _multidropdown
492
+ button = _button
493
+ keybind = _keybind
494
+ label = _label
495
+ separator = _separator
496
+ groupbox = _groupbox
497
+ definePage = _definePage
@@ -0,0 +1,16 @@
1
+ defineComponent('PlayerESP', () => {
2
+ const enabled = signal(false)
3
+ const showName = signal(true)
4
+ const showBox = signal(true)
5
+
6
+ on.heartbeat({ when: enabled, every: 0.05 }, () => {
7
+ // Draw ESP — use Pulse.Draw helpers
8
+ // Pulse.Draw.box(character, { color: [255, 0, 0] })
9
+ })
10
+
11
+ return [
12
+ toggle('Player ESP').bind(enabled),
13
+ toggle('Show Names').bind(showName),
14
+ toggle('Show Boxes').bind(showBox),
15
+ ]
16
+ })
@@ -0,0 +1,17 @@
1
+ defineComponent('FOVChanger', () => {
2
+ const enabled = signal(false)
3
+ const fov = signal(70)
4
+
5
+ const applyFov = () => {
6
+ const cam = (workspace as any).CurrentCamera
7
+ if (cam) cam.FieldOfView = enabled() ? fov() : 70
8
+ }
9
+
10
+ on.signal(enabled, applyFov)
11
+ on.signal(fov, applyFov)
12
+
13
+ return [
14
+ toggle('FOV Changer').bind(enabled),
15
+ slider('Field of View', { min: 1, max: 120, suffix: '°' }).bind(fov),
16
+ ]
17
+ })
@@ -0,0 +1,26 @@
1
+ // SpeedHack — SolidJS-style reactive component.
2
+ // signal() is like SolidJS createSignal; on.* hooks are like createEffect.
3
+
4
+ defineComponent('SpeedHack', () => {
5
+ const enabled = signal(false)
6
+ const speed = signal(16)
7
+
8
+ on.heartbeat({ when: enabled }, () => {
9
+ const char = _PulseGetChar()
10
+ if (!char) return
11
+ const h = char.FindFirstChild('Humanoid') as any
12
+ if (h) h.WalkSpeed = speed()
13
+ })
14
+
15
+ on.respawn(() => {
16
+ const char = _PulseGetChar()
17
+ if (!char) return
18
+ const h = char.FindFirstChild('Humanoid') as any
19
+ if (h) h.WalkSpeed = enabled() ? speed() : 16
20
+ })
21
+
22
+ return [
23
+ toggle('Speed Hack').bind(enabled),
24
+ slider('Walk Speed', { min: 16, max: 250 }).bind(speed),
25
+ ]
26
+ })
@@ -0,0 +1,20 @@
1
+ // Pulse layout — configures the script window.
2
+ // Based on Next.js root layout convention.
3
+ // uiLibrary: 'windui' | 'linoria'
4
+
5
+ export default {
6
+ title: '{NAME}',
7
+ author: '',
8
+ toggleKey: 'RightControl',
9
+ size: [850, 560] as [number, number],
10
+ uiLibrary: 'windui' as 'windui' | 'linoria',
11
+ theme: 'Indigo',
12
+ icon: 'code-2',
13
+ folder: '{NAME}',
14
+ acrylic: true,
15
+ transparency: 0.8,
16
+ openButtonMobileOnly: true,
17
+ openButtonIcon: 'code-2',
18
+ themes: [] as LayoutConfig['themes'],
19
+ compatExclude: [] as string[],
20
+ } satisfies LayoutConfig
@@ -0,0 +1,8 @@
1
+ // Home page tab — Next.js-style file-based routing.
2
+ // Filename prefix (1_) determines tab order.
3
+
4
+ definePage('Home', { icon: 'house' }, () => [
5
+ groupbox('left', 'Player', { icon: 'person', mount: 'SpeedHack' }),
6
+ groupbox('left', 'Player', { icon: 'person', mount: 'FOVChanger' }),
7
+ groupbox('right', 'Visuals', { icon: 'eye', mount: 'PlayerESP' }),
8
+ ])
@@ -1,69 +0,0 @@
1
- -- PlayerESP — example component. Customize or delete this file.
2
- component {
3
- signal enabled = false
4
-
5
- init {
6
- Pulse.Notify.onToggle(PlayerESP.enabled, "Player ESP")
7
-
8
- local _CoreGui = game:GetService("CoreGui")
9
- local _Players = game:GetService("Players")
10
- local _highlights = {}
11
-
12
- local function _addHighlight(player)
13
- if not player.Character then return end
14
- if _highlights[player] then return end
15
- local hl = Instance.new("Highlight")
16
- hl.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop
17
- hl.FillColor = Color3.fromRGB(255, 60, 60)
18
- hl.FillTransparency = 0.4
19
- hl.OutlineColor = Color3.fromRGB(255, 60, 60)
20
- hl.Adornee = player.Character
21
- hl.Parent = _CoreGui
22
- _highlights[player] = hl
23
- Pulse.Log.trace("PlayerESP", "highlighted", { name = player.Name })
24
- end
25
-
26
- local function _removeHighlight(player)
27
- if _highlights[player] then
28
- _highlights[player]:Destroy()
29
- _highlights[player] = nil
30
- end
31
- end
32
-
33
- local function _clearAll()
34
- for _, hl in pairs(_highlights) do hl:Destroy() end
35
- _highlights = {}
36
- end
37
-
38
- local _loop = Pulse.Loop.new(1.0, function()
39
- for _, player in ipairs(func.GetCachedPlayers()) do
40
- if player ~= _Players.LocalPlayer then
41
- _addHighlight(player)
42
- end
43
- end
44
- end)
45
-
46
- local function espLoop(value)
47
- if value then
48
- _loop:start()
49
- Pulse.Log.info("PlayerESP", "started")
50
- else
51
- _loop:stop()
52
- _clearAll()
53
- Pulse.Log.info("PlayerESP", "stopped")
54
- end
55
- end
56
- }
57
-
58
- on enabled { espLoop(v) }
59
-
60
- on CharacterAdded {
61
- if not enabled then return end
62
- _clearAll()
63
- _loop:start()
64
- }
65
-
66
- ui {
67
- toggle "Player ESP" -> enabled tip="Highlight all players through walls"
68
- }
69
- }
@@ -1,20 +0,0 @@
1
- -- FOVChanger — example component. Customize or delete this file.
2
- component {
3
- signal enabled = false
4
- signal fov = 90
5
-
6
- init {
7
- Pulse.Notify.onToggle(FOVChanger.enabled, "Custom FOV")
8
- }
9
-
10
- ui {
11
- toggle "Custom FOV" -> enabled
12
- slider "Field of View" -> fov [30, 120]
13
- }
14
-
15
- on enabled, fov {
16
- guard cam = camera
17
- cam.FieldOfView = enabled and fov or 70
18
- Pulse.Log.info("FOVChanger", "fov set", { fov = cam.FieldOfView })
19
- }
20
- }
@@ -1,25 +0,0 @@
1
- -- SpeedHack — example component. Customize or delete this file.
2
- component {
3
- signal enabled = false
4
- signal speed = 50
5
-
6
- init {
7
- Pulse.Notify.onToggle(SpeedHack.enabled, "Speed Hack")
8
- }
9
-
10
- ui {
11
- toggle "Speed Hack" -> enabled tip="Override character walk speed"
12
- slider "Walk Speed" -> speed [16, 250]
13
- }
14
-
15
- on CharacterAdded {
16
- guard h = humanoid
17
- h.WalkSpeed = enabled and speed or 16
18
- }
19
-
20
- on enabled, speed {
21
- guard h = humanoid
22
- h.WalkSpeed = enabled and speed or 16
23
- Pulse.Log.info("SpeedHack", "walk speed set", { speed = h.WalkSpeed })
24
- }
25
- }
@@ -1,28 +0,0 @@
1
- layout {
2
- title = SCRIPT_NAME,
3
- author = SCRIPT_AUTHOR, -- shown as subtitle under the title
4
- toggle_key = "RightControl", -- Enum.KeyCode name — menu open/close bind
5
- size = { 850, 560 }, -- window width × height in pixels
6
- ui_library = "windui", -- "linoria" | "windui"
7
- theme = "Indigo", -- Dark Light Rose Plant Red Indigo Sky Violet Amber Emerald Midnight Crimson MonokaiPro CottonCandy Mellowsi Rainbow
8
- icon = "code-2", -- Lucide icon name, rbxassetid://, or URL
9
- folder = "{NAME}", -- executor save folder for config persistence
10
- notify_title = SCRIPT_NAME, -- notification header text
11
- transparency = 0.8, -- background transparency: 0 = opaque, 1 = fully transparent
12
- acrylic = true, -- blur effect behind window (executor must support it)
13
- open_button_mobile_only = true, -- false = show floating open button on desktop too
14
- open_button_icon = "code-2", -- Lucide icon for the floating open button
15
-
16
- -- Custom WindUI themes — entries appear in the theme dropdown automatically.
17
- -- Available fields: accent background outline text placeholder button icon
18
- themes = {
19
- -- { name = "MyTheme", accent = "#7c3aed", background = "#0e0c1a", outline = "#1e1b4b",
20
- -- text = "#e8e3ff", placeholder = "#6d6d8a", button = "#1e1b4b", icon = "#a78bfa" },
21
- },
22
-
23
- -- compat_exclude lists modules dropped from build/script.compat.obf.lua
24
- -- (the build for executors that don't support full UNC APIs).
25
- compat_exclude = {
26
- -- "player/UNC.rblua",
27
- }
28
- }
@@ -1,32 +0,0 @@
1
- component {
2
- -- ── Signals ───────────────────────────────────────────────────────────────
3
- signal enabled = false
4
-
5
- -- ── Keybind (toggle on keypress) ──────────────────────────────────────────
6
- -- keybind Enum.KeyCode.X → enabled
7
-
8
- -- ── State change handler ──────────────────────────────────────────────────
9
- on enabled {
10
- if v then
11
- -- feature activated
12
- else
13
- -- feature deactivated
14
- end
15
- }
16
-
17
- -- ── Respawn handler ───────────────────────────────────────────────────────
18
- on Respawn {
19
- -- runs every time the player's character loads
20
- }
21
-
22
- -- ── Per-frame loop ────────────────────────────────────────────────────────
23
- -- on Heartbeat when enabled {
24
- -- guard hrp
25
- -- -- 60fps logic here
26
- -- }
27
-
28
- -- ── Exported functions (callable from other components) ───────────────────
29
- func Toggle() {
30
- enabled(not enabled())
31
- }
32
- }
@@ -1,9 +0,0 @@
1
- page "Home" {
2
- groupbox left "Player" {
3
- mount SpeedHack
4
- mount FOVChanger
5
- }
6
- groupbox right "Visuals" {
7
- mount PlayerESP
8
- }
9
- }