pulse-rb 1.3.1 → 1.4.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/adapters/windui.lua +588 -8
- package/bin/rb.js +0 -0
- package/dist/index.js +101 -41
- package/package.json +11 -8
- package/pulse/runtime.lua +28 -8
- package/templates/component.ts +11 -0
- package/templates/globals.lua +10 -26
- package/templates/layout.ts +1 -2
- package/templates/page_home.ts +3 -6
- package/templates/tsconfig.json +11 -0
package/adapters/windui.lua
CHANGED
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
-- Docs: footagesus.github.io/treehub-web/docs/windui
|
|
4
4
|
-- Version: 1.6.64-fix (pinned)
|
|
5
5
|
|
|
6
|
-
-- Executor loadstring() environments are sandboxed —
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
local _P = ...
|
|
6
|
+
-- Executor loadstring() environments are sandboxed — each chunk has its OWN environment
|
|
7
|
+
-- table, separate from _G. Global reads in this chunk go through getfenv(1), not _G.
|
|
8
|
+
-- The compiler passes (_P, _PULSE_LAYOUT) as arguments.
|
|
9
|
+
local _P, _PULSE_LAYOUT = ...
|
|
10
10
|
if type(_P) == "table" then
|
|
11
|
-
|
|
11
|
+
local _env = (getfenv and getfenv(1)) or _ENV or _G
|
|
12
|
+
for _k, _v in pairs(_P) do _env[_k] = _v end
|
|
12
13
|
end
|
|
14
|
+
_PULSE_LAYOUT = _PULSE_LAYOUT or {}
|
|
15
|
+
|
|
16
|
+
-- Capture bundle table references as upvalues immediately so the startup block
|
|
17
|
+
-- can access them without global lookups (which are environment-table-dependent
|
|
18
|
+
-- in executor sandboxes and may fail in task.spawn callbacks).
|
|
19
|
+
local _BundleComponents = type(_P) == "table" and _P.Components or {}
|
|
20
|
+
local _BundlePages = type(_P) == "table" and _P._Pages or {}
|
|
21
|
+
local _BundlePulse = type(_P) == "table" and _P.Pulse or Pulse
|
|
13
22
|
|
|
14
23
|
local _WINDUI_URL = "https://github.com/Footagesus/WindUI/releases/download/1.6.64-fix/main.lua"
|
|
15
24
|
local _PULSE_LOGO = "https://pulse-rb.vercel.app/img/logo.svg"
|
|
@@ -208,7 +217,12 @@ function _UIAdapter:addToggle(gb, id, opts)
|
|
|
208
217
|
Desc = opts.tip or nil,
|
|
209
218
|
Value = opts.signal(),
|
|
210
219
|
Flag = id,
|
|
211
|
-
Callback = function(v)
|
|
220
|
+
Callback = function(v)
|
|
221
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
222
|
+
_BundlePulse.Log.debug("ui", "toggle "..tostring(id).." → "..tostring(v))
|
|
223
|
+
end
|
|
224
|
+
opts.signal(v)
|
|
225
|
+
end,
|
|
212
226
|
})
|
|
213
227
|
_widgets[id] = elem
|
|
214
228
|
_G.Toggles = _G.Toggles or {}
|
|
@@ -238,7 +252,12 @@ function _UIAdapter:addSlider(gb, id, opts)
|
|
|
238
252
|
Step = step,
|
|
239
253
|
Value = { Min = min, Max = max, Default = def },
|
|
240
254
|
Flag = id,
|
|
241
|
-
Callback = function(v)
|
|
255
|
+
Callback = function(v)
|
|
256
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
257
|
+
_BundlePulse.Log.debug("ui", "slider "..tostring(id).." → "..tostring(v))
|
|
258
|
+
end
|
|
259
|
+
opts.signal(v)
|
|
260
|
+
end,
|
|
242
261
|
})
|
|
243
262
|
_widgets[id] = elem
|
|
244
263
|
_G.Options = _G.Options or {}
|
|
@@ -355,7 +374,7 @@ function _UIAdapter:mount(component, gb)
|
|
|
355
374
|
if not ui then return end
|
|
356
375
|
for _, w in ipairs(ui) do
|
|
357
376
|
local t = w.type
|
|
358
|
-
local sig =
|
|
377
|
+
local sig = w.signal -- PulseSignal stored directly by .bind(sig) in TS widget builders
|
|
359
378
|
if t == "toggle" then
|
|
360
379
|
self:addToggle(gb, w.id, { label = w.label, signal = sig, tip = w.tip })
|
|
361
380
|
elseif t == "slider" then
|
|
@@ -507,3 +526,564 @@ function Pulse.UI.section(container, title, icon, opened)
|
|
|
507
526
|
function proxy:section(...) return Pulse.UI.section(self, ...) end
|
|
508
527
|
return proxy
|
|
509
528
|
end
|
|
529
|
+
|
|
530
|
+
-- ── Startup — create window, render pages, build Settings tab ─────────────────
|
|
531
|
+
-- Runs after user code has registered all components and pages (task.spawn defers
|
|
532
|
+
-- execution to the next frame, by which point all top-level TS calls have run).
|
|
533
|
+
|
|
534
|
+
task.spawn(function()
|
|
535
|
+
task.wait(0) -- yield one frame so definePage/defineComponent calls finish
|
|
536
|
+
|
|
537
|
+
local L = _PULSE_LAYOUT
|
|
538
|
+
|
|
539
|
+
-- Resolve toggle key
|
|
540
|
+
local _tkName = (L.toggleKey and L.toggleKey ~= "") and L.toggleKey or "RightControl"
|
|
541
|
+
local _tk = pcall(function() return Enum.KeyCode[_tkName] end) and Enum.KeyCode[_tkName]
|
|
542
|
+
_PULSE_TOGGLE_KEY = _tk
|
|
543
|
+
_PULSE_DEFAULT_THEME = L.theme or "Dark"
|
|
544
|
+
|
|
545
|
+
-- Create the Wind UI window
|
|
546
|
+
local _win = _UIAdapter:CreateWindow(
|
|
547
|
+
L.title or "Hub",
|
|
548
|
+
L.sizeW or 950,
|
|
549
|
+
L.sizeH or 600,
|
|
550
|
+
{
|
|
551
|
+
icon = (L.icon and L.icon ~= "") and L.icon or nil,
|
|
552
|
+
author = L.author or nil,
|
|
553
|
+
theme = L.theme or "Dark",
|
|
554
|
+
folder = (L.folder and L.folder ~= "") and L.folder or "Hub",
|
|
555
|
+
acrylic = L.acrylic or false,
|
|
556
|
+
transparency = L.transparency or 0.8,
|
|
557
|
+
toggleKey = _tk,
|
|
558
|
+
openButtonMobileOnly = L.openButtonMobileOnly or false,
|
|
559
|
+
openButtonIcon = (L.openButtonIcon and L.openButtonIcon ~= "") and L.openButtonIcon or nil,
|
|
560
|
+
}
|
|
561
|
+
)
|
|
562
|
+
_UIAdapter:SetToggleKey(_tk)
|
|
563
|
+
|
|
564
|
+
-- ── Home tab ──────────────────────────────────────────────────────────────
|
|
565
|
+
-- Always the first tab: player welcome, script info, executor/game context.
|
|
566
|
+
|
|
567
|
+
-- Player info
|
|
568
|
+
local _PS2 = game:GetService("Players")
|
|
569
|
+
local _LP2 = _PS2.LocalPlayer
|
|
570
|
+
local _dspName = (_LP2 and _LP2.DisplayName) or "Player"
|
|
571
|
+
local _usrName = (_LP2 and _LP2.Name) or ""
|
|
572
|
+
|
|
573
|
+
-- Avatar thumbnail (GetUserThumbnailAsync yields — fine inside task.spawn)
|
|
574
|
+
local _avatarUrl = ""
|
|
575
|
+
pcall(function()
|
|
576
|
+
if _LP2 then
|
|
577
|
+
_avatarUrl = _PS2:GetUserThumbnailAsync(
|
|
578
|
+
_LP2.UserId,
|
|
579
|
+
Enum.ThumbnailType.HeadShot,
|
|
580
|
+
Enum.ThumbnailSize.Size100x100
|
|
581
|
+
)
|
|
582
|
+
end
|
|
583
|
+
end)
|
|
584
|
+
|
|
585
|
+
-- Executor detection
|
|
586
|
+
local _execName = "Unknown"
|
|
587
|
+
local _execVer = ""
|
|
588
|
+
pcall(function()
|
|
589
|
+
if type(rawget(_G, "identifyexecutor")) == "function" then
|
|
590
|
+
local n, v = identifyexecutor()
|
|
591
|
+
_execName = tostring(n or "Unknown")
|
|
592
|
+
_execVer = tostring(v or "")
|
|
593
|
+
elseif rawget(_G, "syn") then _execName = "Synapse X"
|
|
594
|
+
elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
|
|
595
|
+
elseif rawget(_G, "Delta") then _execName = "Delta"
|
|
596
|
+
elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
|
|
597
|
+
elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
|
|
598
|
+
end
|
|
599
|
+
end)
|
|
600
|
+
|
|
601
|
+
-- Game name (GetProductInfo yields)
|
|
602
|
+
local _gameName = "PlaceId " .. tostring(game.PlaceId)
|
|
603
|
+
pcall(function()
|
|
604
|
+
local info = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId)
|
|
605
|
+
if info and info.Name then _gameName = info.Name end
|
|
606
|
+
end)
|
|
607
|
+
|
|
608
|
+
local _homeTab = _win:AddTab("Home", "house")
|
|
609
|
+
|
|
610
|
+
-- Left column ──────────────────────────────────────────────────────────────
|
|
611
|
+
|
|
612
|
+
-- Welcome / player section
|
|
613
|
+
local _welcomeGb = _homeTab:AddLeftGroupbox("Welcome", "person")
|
|
614
|
+
if _avatarUrl ~= "" then
|
|
615
|
+
-- WindUI Image element — silently skipped if unsupported
|
|
616
|
+
pcall(function()
|
|
617
|
+
_welcomeGb._container:Image({
|
|
618
|
+
Source = _avatarUrl,
|
|
619
|
+
Size = UDim2.fromOffset(96, 96),
|
|
620
|
+
})
|
|
621
|
+
end)
|
|
622
|
+
end
|
|
623
|
+
_welcomeGb._container:Paragraph({
|
|
624
|
+
Title = _dspName,
|
|
625
|
+
Desc = "@" .. _usrName,
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
if L.description and L.description ~= "" then
|
|
629
|
+
_welcomeGb._container:Divider()
|
|
630
|
+
_welcomeGb._container:Paragraph({ Title = "", Desc = L.description })
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
if L.discord and L.discord ~= "" then
|
|
634
|
+
local _discGb = _homeTab:AddLeftGroupbox("Community", "message-circle")
|
|
635
|
+
_discGb._container:Button({
|
|
636
|
+
Title = "Join Discord",
|
|
637
|
+
Desc = "Tap to copy the invite link",
|
|
638
|
+
Callback = function()
|
|
639
|
+
local ok = false
|
|
640
|
+
if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
|
|
641
|
+
if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
|
|
642
|
+
if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
|
|
643
|
+
_PulseNotify(ok and "Discord link copied!" or "No clipboard API available", 3)
|
|
644
|
+
end,
|
|
645
|
+
})
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
-- Right column ─────────────────────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
-- Script metadata
|
|
651
|
+
local _scriptGb = _homeTab:AddRightGroupbox("Script", "code-2")
|
|
652
|
+
local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
|
|
653
|
+
local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
|
|
654
|
+
local _metaParts = {}
|
|
655
|
+
if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
|
|
656
|
+
if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
|
|
657
|
+
_scriptGb._container:Paragraph({
|
|
658
|
+
Title = L.title or "Hub",
|
|
659
|
+
Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "—",
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
-- System info
|
|
663
|
+
local _sysGb = _homeTab:AddRightGroupbox("System", "monitor")
|
|
664
|
+
_sysGb._container:Paragraph({
|
|
665
|
+
Title = "Executor",
|
|
666
|
+
Desc = _execName .. (_execVer ~= "" and (" " .. _execVer) or ""),
|
|
667
|
+
})
|
|
668
|
+
_sysGb._container:Paragraph({ Title = "Game", Desc = _gameName })
|
|
669
|
+
|
|
670
|
+
-- Unsupported executors (optional — set L.unsupported in layout config)
|
|
671
|
+
if L.unsupported and L.unsupported ~= "" then
|
|
672
|
+
local _unsupList = {}
|
|
673
|
+
if type(L.unsupported) == "table" then
|
|
674
|
+
for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
|
|
675
|
+
elseif type(L.unsupported) == "string" then
|
|
676
|
+
for part in L.unsupported:gmatch("[^,]+") do
|
|
677
|
+
_unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
if #_unsupList > 0 then
|
|
681
|
+
local _unsupGb = _homeTab:AddRightGroupbox("Not Supported", "alert-triangle")
|
|
682
|
+
_unsupGb._container:Paragraph({
|
|
683
|
+
Title = "Not compatible with:",
|
|
684
|
+
Desc = table.concat(_unsupList, ", "),
|
|
685
|
+
})
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
-- Render user pages (upvalue _BundlePages — avoids global lookup in executor sandbox)
|
|
690
|
+
for _, page in ipairs(_BundlePages) do
|
|
691
|
+
local tab = _win:AddTab(page.title, page.icon)
|
|
692
|
+
for _, gb in ipairs(page.layout or {}) do
|
|
693
|
+
local container
|
|
694
|
+
if gb.side == "left" then
|
|
695
|
+
container = tab:AddLeftGroupbox(gb.title, gb.icon)
|
|
696
|
+
else
|
|
697
|
+
container = tab:AddRightGroupbox(gb.title, gb.icon)
|
|
698
|
+
end
|
|
699
|
+
if gb.mount then
|
|
700
|
+
local comp = _BundleComponents[gb.mount]
|
|
701
|
+
if comp then
|
|
702
|
+
local ok, err = pcall(function() _UIAdapter:mount(comp, container) end)
|
|
703
|
+
if not ok then
|
|
704
|
+
warn("[Pulse] mount '" .. gb.mount .. "' error: " .. tostring(err))
|
|
705
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
706
|
+
_BundlePulse.Log.error("ui", "mount '"..gb.mount.."' failed: "..tostring(err))
|
|
707
|
+
end
|
|
708
|
+
else
|
|
709
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
710
|
+
local nw = comp._ui and #comp._ui or 0
|
|
711
|
+
_BundlePulse.Log.debug("ui", "mounted '"..gb.mount.."' ("..nw.." widgets)")
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
else
|
|
715
|
+
warn("[Pulse] component '" .. gb.mount .. "' not found in registry")
|
|
716
|
+
if _BundlePulse and _BundlePulse.Log then
|
|
717
|
+
_BundlePulse.Log.error("ui", "component '"..gb.mount.."' not in registry")
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
else
|
|
721
|
+
for _, w in ipairs(gb.widgets or {}) do
|
|
722
|
+
pcall(function() _UIAdapter:mount({ _ui = {w}, _name = gb.title }, container) end)
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
-- ── Settings tab ──────────────────────────────────────────────────────────
|
|
729
|
+
local _settingsTab = _win:AddTab("Settings", "settings")
|
|
730
|
+
|
|
731
|
+
-- Appearance groupbox
|
|
732
|
+
local gb_appear = _settingsTab:AddLeftGroupbox("Appearance")
|
|
733
|
+
local _themeValues = _UIAdapter:GetThemeNames()
|
|
734
|
+
or { "Amber","CottonCandy","Crimson","Dark","Emerald","Indigo","Light",
|
|
735
|
+
"Mellowsi","Midnight","MonokaiPro","Plant","Rainbow","Red","Rose","Sky","Violet" }
|
|
736
|
+
gb_appear._container:Dropdown({
|
|
737
|
+
Title = "Theme",
|
|
738
|
+
Values = _themeValues,
|
|
739
|
+
Value = _PULSE_DEFAULT_THEME,
|
|
740
|
+
Flag = "Settings_theme",
|
|
741
|
+
Callback = function(v) _UIAdapter:SetTheme(v) end,
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
-- Config groupbox
|
|
745
|
+
local gb_cfg = _settingsTab:AddLeftGroupbox("Configuration")
|
|
746
|
+
_UIAdapter:addParagraph(gb_cfg,
|
|
747
|
+
"Config is stored in your executor's filesystem.",
|
|
748
|
+
"Save writes the current state. Load restores the last saved state."
|
|
749
|
+
)
|
|
750
|
+
gb_cfg._container:Button({
|
|
751
|
+
Title = "Save Config",
|
|
752
|
+
Icon = "save",
|
|
753
|
+
Callback = function()
|
|
754
|
+
if _UIAdapter.Config then
|
|
755
|
+
local ok, err = pcall(function() _UIAdapter.Config:Save() end)
|
|
756
|
+
_PulseNotify(ok and "Config saved." or ("Save failed: " .. tostring(err)), 3)
|
|
757
|
+
else
|
|
758
|
+
_PulseNotify("Config manager unavailable.", 3)
|
|
759
|
+
end
|
|
760
|
+
end,
|
|
761
|
+
})
|
|
762
|
+
gb_cfg._container:Button({
|
|
763
|
+
Title = "Load Config",
|
|
764
|
+
Icon = "folder-open",
|
|
765
|
+
Callback = function()
|
|
766
|
+
if _UIAdapter.Config then
|
|
767
|
+
local ok, err = pcall(function() _UIAdapter.Config:Load() end)
|
|
768
|
+
_PulseNotify(ok and "Config loaded." or ("Load failed: " .. tostring(err)), 3)
|
|
769
|
+
else
|
|
770
|
+
_PulseNotify("Config manager unavailable.", 3)
|
|
771
|
+
end
|
|
772
|
+
end,
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
-- Menu settings groupbox
|
|
776
|
+
local gb_menu = _settingsTab:AddRightGroupbox("Menu Settings")
|
|
777
|
+
_UIAdapter:addButton(gb_menu, {
|
|
778
|
+
label = "Destroy UI",
|
|
779
|
+
action = function() _PulseDestroy() end,
|
|
780
|
+
tip = "Unloads the script and removes all UI",
|
|
781
|
+
})
|
|
782
|
+
_UIAdapter:addKeybind(gb_menu, "Settings_MenuKeybind", {
|
|
783
|
+
label = "Menu Toggle",
|
|
784
|
+
key = _PULSE_TOGGLE_KEY,
|
|
785
|
+
action = function() _UIAdapter:ToggleWindow() end,
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
-- Auto-load saved config on startup
|
|
789
|
+
task.spawn(function()
|
|
790
|
+
task.wait(1.5)
|
|
791
|
+
if _UIAdapter.Config then
|
|
792
|
+
local ok = pcall(function() _UIAdapter.Config:Load() end)
|
|
793
|
+
if ok and _BundlePulse and _BundlePulse.Log then _BundlePulse.Log.info("settings", "config auto-loaded") end
|
|
794
|
+
end
|
|
795
|
+
end)
|
|
796
|
+
|
|
797
|
+
-- ── Dev tab (--dev builds only) ───────────────────────────────────────────
|
|
798
|
+
if L.dev then
|
|
799
|
+
local _devTab
|
|
800
|
+
pcall(function() _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" }) end)
|
|
801
|
+
if _devTab then
|
|
802
|
+
local _devCol
|
|
803
|
+
pcall(function() _devCol = _devTab:VStack({}) end)
|
|
804
|
+
_devCol = _devCol or _devTab
|
|
805
|
+
|
|
806
|
+
-- Tools section
|
|
807
|
+
local _toolSect
|
|
808
|
+
pcall(function()
|
|
809
|
+
_toolSect = _devCol:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
|
|
810
|
+
end)
|
|
811
|
+
|
|
812
|
+
local _TOOLS = {
|
|
813
|
+
{ name="Hydroxide", desc="Remote spy with web interface", url=nil,
|
|
814
|
+
load=function()
|
|
815
|
+
local function webImport(f)
|
|
816
|
+
loadstring(game:HttpGetAsync(("https://raw.githubusercontent.com/Upbolt/Hydroxide/revision/%s.lua"):format(f)), f..".lua")()
|
|
817
|
+
end
|
|
818
|
+
webImport("init"); webImport("ui/main")
|
|
819
|
+
end },
|
|
820
|
+
{ name="Infinite Yield", desc="Admin / exploit command panel", url="https://raw.githubusercontent.com/DarkNetworks/Infinite-Yield/main/latest.lua" },
|
|
821
|
+
{ name="Dex Explorer", desc="Browse and inspect the live game tree",url="https://raw.githubusercontent.com/infyiff/backup/main/dex.lua" },
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if _toolSect then
|
|
825
|
+
for _, tool in ipairs(_TOOLS) do
|
|
826
|
+
local _launched = false
|
|
827
|
+
local _btn
|
|
828
|
+
pcall(function()
|
|
829
|
+
_btn = _toolSect:Button({
|
|
830
|
+
Title = tool.name,
|
|
831
|
+
Desc = tool.desc,
|
|
832
|
+
Callback = function()
|
|
833
|
+
if _launched then _PulseNotify("Already running: "..tool.name, 2); return end
|
|
834
|
+
pcall(function() _btn:SetTitle(tool.name.." · loading…") end)
|
|
835
|
+
task.spawn(function()
|
|
836
|
+
local ok, err = pcall(function()
|
|
837
|
+
if tool.load then
|
|
838
|
+
tool.load()
|
|
839
|
+
else
|
|
840
|
+
local src = game:HttpGet(tool.url)
|
|
841
|
+
local fn, ce = loadstring(src)
|
|
842
|
+
if not fn then error(ce) end
|
|
843
|
+
fn()
|
|
844
|
+
end
|
|
845
|
+
end)
|
|
846
|
+
if ok then
|
|
847
|
+
_launched = true
|
|
848
|
+
pcall(function() _btn:SetTitle("✓ "..tool.name) end)
|
|
849
|
+
_PulseNotify(tool.name.." launched", 3)
|
|
850
|
+
else
|
|
851
|
+
pcall(function() _btn:SetTitle(tool.name.." · failed") end)
|
|
852
|
+
_PulseNotify(tool.name.." failed: "..tostring(err):sub(1,60), 4)
|
|
853
|
+
task.delay(3, function()
|
|
854
|
+
if not _launched then pcall(function() _btn:SetTitle(tool.name) end) end
|
|
855
|
+
end)
|
|
856
|
+
end
|
|
857
|
+
end)
|
|
858
|
+
end,
|
|
859
|
+
})
|
|
860
|
+
end)
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
-- Scanner section
|
|
865
|
+
local _scanSect
|
|
866
|
+
pcall(function()
|
|
867
|
+
_scanSect = _devCol:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
|
|
868
|
+
end)
|
|
869
|
+
|
|
870
|
+
if _scanSect then
|
|
871
|
+
local _lastScan = ""
|
|
872
|
+
local _statusPara
|
|
873
|
+
pcall(function()
|
|
874
|
+
_statusPara = _scanSect:Paragraph({
|
|
875
|
+
Title = "Game Scanner",
|
|
876
|
+
Desc = "Auto-scan runs on inject. Results copied to clipboard.",
|
|
877
|
+
})
|
|
878
|
+
end)
|
|
879
|
+
|
|
880
|
+
local function _devCopy(text)
|
|
881
|
+
local ok = false
|
|
882
|
+
if not ok then pcall(function() setclipboard(text); ok=true end) end
|
|
883
|
+
if not ok then pcall(function() toclipboard(text); ok=true end) end
|
|
884
|
+
if not ok then pcall(function() writeclipboard(text); ok=true end) end
|
|
885
|
+
return ok
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
local _ALIASES = {
|
|
889
|
+
{ "Players.LocalPlayer.PlayerGui.", "PG." },
|
|
890
|
+
{ "Players.LocalPlayer.", "LP." },
|
|
891
|
+
{ "ReplicatedStorage.", "RS." },
|
|
892
|
+
{ "ReplicatedFirst.", "RF." },
|
|
893
|
+
{ "workspace.", "WS." },
|
|
894
|
+
}
|
|
895
|
+
local function _compress(obj)
|
|
896
|
+
local parts = {}; local cur = obj
|
|
897
|
+
while cur and cur ~= game do table.insert(parts, 1, cur.Name); cur = cur.Parent end
|
|
898
|
+
local s = table.concat(parts, ".")
|
|
899
|
+
for _, pair in ipairs(_ALIASES) do
|
|
900
|
+
if s:sub(1, #pair[1]) == pair[1] then return pair[2]..s:sub(#pair[1]+1) end
|
|
901
|
+
end
|
|
902
|
+
return s
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
local function _doScan()
|
|
906
|
+
pcall(function() _statusPara:SetDesc("Scanning…") end)
|
|
907
|
+
task.spawn(function()
|
|
908
|
+
local out = {}
|
|
909
|
+
local RS = game:GetService("ReplicatedStorage")
|
|
910
|
+
local gameName = "?"
|
|
911
|
+
pcall(function()
|
|
912
|
+
gameName = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId).Name
|
|
913
|
+
end)
|
|
914
|
+
table.insert(out, ("[%s] pid:%d"):format(gameName, game.PlaceId))
|
|
915
|
+
|
|
916
|
+
local remotes = {}
|
|
917
|
+
local function _scanRemotes(inst, depth)
|
|
918
|
+
if depth > 8 then return end
|
|
919
|
+
local ok, ch = pcall(function() return inst:GetChildren() end)
|
|
920
|
+
if not ok then return end
|
|
921
|
+
for _, c in ipairs(ch) do
|
|
922
|
+
local cls = c.ClassName
|
|
923
|
+
if cls == "RemoteEvent" or cls == "RemoteFunction" then
|
|
924
|
+
table.insert(remotes, { p=_compress(c), t=cls=="RemoteEvent" and "RE" or "RF" })
|
|
925
|
+
end
|
|
926
|
+
if c:IsA("Folder") or c:IsA("Configuration") or c:IsA("Model") then
|
|
927
|
+
_scanRemotes(c, depth+1)
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
pcall(_scanRemotes, RS, 0)
|
|
932
|
+
table.sort(remotes, function(a,b) return a.p < b.p end)
|
|
933
|
+
table.insert(out, ("REMOTES (%d)"):format(#remotes))
|
|
934
|
+
for i, r in ipairs(remotes) do
|
|
935
|
+
if i > 50 then table.insert(out, (" ...+%d more"):format(#remotes-50)); break end
|
|
936
|
+
table.insert(out, (" %-52s %s"):format(r.p, r.t))
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
local txt = table.concat(out, "\n")
|
|
940
|
+
_lastScan = txt
|
|
941
|
+
local copied = _devCopy(txt)
|
|
942
|
+
local summary = ("%d remotes%s"):format(#remotes, copied and " · copied" or "")
|
|
943
|
+
pcall(function()
|
|
944
|
+
_statusPara:SetTitle("Scan "..os.date("%H:%M:%S"))
|
|
945
|
+
_statusPara:SetDesc(summary)
|
|
946
|
+
end)
|
|
947
|
+
if copied then _PulseNotify("Scan complete · "..summary, 4) end
|
|
948
|
+
end)
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
pcall(function()
|
|
952
|
+
_scanSect:Button({
|
|
953
|
+
Title="Scan Now", Desc="Scan remotes and copy to clipboard",
|
|
954
|
+
Callback = _doScan,
|
|
955
|
+
})
|
|
956
|
+
end)
|
|
957
|
+
pcall(function()
|
|
958
|
+
_scanSect:Button({
|
|
959
|
+
Title="Copy Last Results", Desc="Re-copy most recent scan",
|
|
960
|
+
Callback = function()
|
|
961
|
+
if _lastScan == "" then _PulseNotify("No scan yet", 2); return end
|
|
962
|
+
local ok = _devCopy(_lastScan)
|
|
963
|
+
_PulseNotify(ok and "Copied" or "No clipboard function", 2)
|
|
964
|
+
end,
|
|
965
|
+
})
|
|
966
|
+
end)
|
|
967
|
+
|
|
968
|
+
-- Auto-scan on inject
|
|
969
|
+
task.spawn(function()
|
|
970
|
+
if not game:IsLoaded() then game.Loaded:Wait() end
|
|
971
|
+
task.wait(2)
|
|
972
|
+
_doScan()
|
|
973
|
+
end)
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
-- Info section — framework version + signal counts for debugging
|
|
977
|
+
local _infoSect
|
|
978
|
+
pcall(function()
|
|
979
|
+
_infoSect = _devCol:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
|
|
980
|
+
end)
|
|
981
|
+
if _infoSect then
|
|
982
|
+
local nPages = #_BundlePages
|
|
983
|
+
local nComps = 0
|
|
984
|
+
for _ in pairs(_BundleComponents) do nComps = nComps + 1 end
|
|
985
|
+
pcall(function()
|
|
986
|
+
_infoSect:Paragraph({
|
|
987
|
+
Title = "Framework",
|
|
988
|
+
Desc = ("pages=%d components=%d theme=%s"):format(nPages, nComps, L.theme or "?"),
|
|
989
|
+
})
|
|
990
|
+
end)
|
|
991
|
+
for cname, comp in pairs(_BundleComponents) do
|
|
992
|
+
if type(comp) == "table" and comp._ui then
|
|
993
|
+
pcall(function()
|
|
994
|
+
_infoSect:Paragraph({
|
|
995
|
+
Title = cname,
|
|
996
|
+
Desc = ("%d widget(s)"):format(#comp._ui),
|
|
997
|
+
})
|
|
998
|
+
end)
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
-- Console section — controls for the Roblox developer console (F9).
|
|
1004
|
+
-- "Clear" wipes the output so only your next prints are visible.
|
|
1005
|
+
-- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
|
|
1006
|
+
-- them cleanly after a clear without any engine noise in the way.
|
|
1007
|
+
-- "Copy Logs" copies the full log buffer to clipboard.
|
|
1008
|
+
local _consoleSect
|
|
1009
|
+
pcall(function()
|
|
1010
|
+
_consoleSect = _devCol:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
|
|
1011
|
+
end)
|
|
1012
|
+
if _consoleSect and _BundlePulse and _BundlePulse.Log then
|
|
1013
|
+
local _Log = _BundlePulse.Log
|
|
1014
|
+
|
|
1015
|
+
-- Clear console: try executor functions, fall back to blank lines separator.
|
|
1016
|
+
local function _clearConsole()
|
|
1017
|
+
local cleared = false
|
|
1018
|
+
if not cleared then pcall(function() consoleclear(); cleared=true end) end
|
|
1019
|
+
if not cleared then pcall(function() clearoutput(); cleared=true end) end
|
|
1020
|
+
if not cleared then pcall(function()
|
|
1021
|
+
game:GetService("LogService"):ClearOutput(); cleared=true
|
|
1022
|
+
end) end
|
|
1023
|
+
if not cleared then
|
|
1024
|
+
-- visual separator when no clear API is available
|
|
1025
|
+
print(("\n"):rep(40))
|
|
1026
|
+
print("──────────── console cleared ────────────")
|
|
1027
|
+
end
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
local function _copyLogs()
|
|
1031
|
+
local txt = _Log.dump()
|
|
1032
|
+
local ok = false
|
|
1033
|
+
if not ok then pcall(function() setclipboard(txt); ok=true end) end
|
|
1034
|
+
if not ok then pcall(function() toclipboard(txt); ok=true end) end
|
|
1035
|
+
if not ok then pcall(function() writeclipboard(txt); ok=true end) end
|
|
1036
|
+
_PulseNotify(ok and "Logs copied to clipboard" or "No clipboard API available", 3)
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
local function _printLogs()
|
|
1040
|
+
local entries = _Log.getNewEntries(0)
|
|
1041
|
+
print("──────────── Pulse Logs (" .. #entries .. ") ────────────")
|
|
1042
|
+
for _, e in ipairs(entries) do
|
|
1043
|
+
if e.n >= 3 then warn(e.line) else print(e.line) end
|
|
1044
|
+
end
|
|
1045
|
+
print("─────────────────────────────────────────")
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
pcall(function()
|
|
1049
|
+
_consoleSect:Button({
|
|
1050
|
+
Title = "Clear Console",
|
|
1051
|
+
Desc = "Wipe F9 output — then use Print Logs to see only yours",
|
|
1052
|
+
Callback = _clearConsole,
|
|
1053
|
+
})
|
|
1054
|
+
end)
|
|
1055
|
+
pcall(function()
|
|
1056
|
+
_consoleSect:Button({
|
|
1057
|
+
Title = "Print Logs",
|
|
1058
|
+
Desc = "Re-print all Pulse.Log entries to F9 console",
|
|
1059
|
+
Callback = _printLogs,
|
|
1060
|
+
})
|
|
1061
|
+
end)
|
|
1062
|
+
pcall(function()
|
|
1063
|
+
_consoleSect:Button({
|
|
1064
|
+
Title = "Copy Logs",
|
|
1065
|
+
Desc = "Copy full log buffer to clipboard",
|
|
1066
|
+
Callback = _copyLogs,
|
|
1067
|
+
})
|
|
1068
|
+
end)
|
|
1069
|
+
pcall(function()
|
|
1070
|
+
_consoleSect:Button({
|
|
1071
|
+
Title = "Clear Log Buffer",
|
|
1072
|
+
Desc = "Reset the in-memory Pulse.Log buffer",
|
|
1073
|
+
Callback = function()
|
|
1074
|
+
_Log.clear()
|
|
1075
|
+
_PulseNotify("Log buffer cleared", 2)
|
|
1076
|
+
end,
|
|
1077
|
+
})
|
|
1078
|
+
end)
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
-- Re-select first tab (Home) — Settings is last so Wind UI leaves it active
|
|
1084
|
+
task.defer(function()
|
|
1085
|
+
if _UIAdapter._firstTab then
|
|
1086
|
+
pcall(function() _UIAdapter._firstTab:Select() end)
|
|
1087
|
+
end
|
|
1088
|
+
end)
|
|
1089
|
+
end)
|
package/bin/rb.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -56,7 +56,7 @@ var RB_VERSION, RB_HOME, IRONBREW2_DIR, IRONBREW2_REPO, ALWAYS_EXCLUDE, VALID_UI
|
|
|
56
56
|
var init_constants = __esm({
|
|
57
57
|
"src/constants.ts"() {
|
|
58
58
|
init_cjs_shims();
|
|
59
|
-
RB_VERSION = "1.
|
|
59
|
+
RB_VERSION = "1.4.0";
|
|
60
60
|
RB_HOME = path.join(os.homedir(), ".rb");
|
|
61
61
|
path.join(RB_HOME, "bin");
|
|
62
62
|
IRONBREW2_DIR = path.join(RB_HOME, "ironbrew2");
|
|
@@ -1193,6 +1193,59 @@ end)
|
|
|
1193
1193
|
}
|
|
1194
1194
|
return "windui";
|
|
1195
1195
|
}
|
|
1196
|
+
readLayoutConfig() {
|
|
1197
|
+
const layoutTs = path.join(this.srcDir, "layout.ts");
|
|
1198
|
+
const cfg = {
|
|
1199
|
+
title: "Script Hub",
|
|
1200
|
+
author: "",
|
|
1201
|
+
toggleKey: "RightControl",
|
|
1202
|
+
sizeW: 950,
|
|
1203
|
+
sizeH: 600,
|
|
1204
|
+
theme: "Dark",
|
|
1205
|
+
icon: "",
|
|
1206
|
+
folder: "Hub",
|
|
1207
|
+
acrylic: false,
|
|
1208
|
+
transparency: 0.8,
|
|
1209
|
+
openButtonMobileOnly: false,
|
|
1210
|
+
openButtonIcon: "",
|
|
1211
|
+
description: "",
|
|
1212
|
+
discord: "",
|
|
1213
|
+
version: ""
|
|
1214
|
+
};
|
|
1215
|
+
if (!fs.existsSync(layoutTs)) return cfg;
|
|
1216
|
+
const src = fs.readFileSync(layoutTs, "utf8");
|
|
1217
|
+
const str = (key) => {
|
|
1218
|
+
const m = src.match(new RegExp(key + `\\s*:\\s*['"]([^'"]+)['"]`));
|
|
1219
|
+
if (m) cfg[key] = m[1];
|
|
1220
|
+
};
|
|
1221
|
+
const bool = (key) => {
|
|
1222
|
+
const m = src.match(new RegExp(key + `\\s*:\\s*(true|false)`));
|
|
1223
|
+
if (m) cfg[key] = m[1] === "true";
|
|
1224
|
+
};
|
|
1225
|
+
const num = (key) => {
|
|
1226
|
+
const m = src.match(new RegExp(key + `\\s*:\\s*([\\d.]+)`));
|
|
1227
|
+
if (m) cfg[key] = parseFloat(m[1]);
|
|
1228
|
+
};
|
|
1229
|
+
str("title");
|
|
1230
|
+
str("author");
|
|
1231
|
+
str("toggleKey");
|
|
1232
|
+
str("theme");
|
|
1233
|
+
str("icon");
|
|
1234
|
+
str("folder");
|
|
1235
|
+
str("openButtonIcon");
|
|
1236
|
+
str("description");
|
|
1237
|
+
str("discord");
|
|
1238
|
+
str("version");
|
|
1239
|
+
bool("acrylic");
|
|
1240
|
+
bool("openButtonMobileOnly");
|
|
1241
|
+
num("transparency");
|
|
1242
|
+
const sz = src.match(/size\s*:\s*\[(\d+)\s*,\s*(\d+)\]/);
|
|
1243
|
+
if (sz) {
|
|
1244
|
+
cfg["sizeW"] = parseInt(sz[1]);
|
|
1245
|
+
cfg["sizeH"] = parseInt(sz[2]);
|
|
1246
|
+
}
|
|
1247
|
+
return cfg;
|
|
1248
|
+
}
|
|
1196
1249
|
resolveUi(ui) {
|
|
1197
1250
|
if (!ui) ui = this.getUiLibrary();
|
|
1198
1251
|
if (!VALID_UI_LIBS.has(ui)) throw new Error(`Unknown UI library '${ui}'. Valid: ${[...VALID_UI_LIBS].sort().join(", ")}`);
|
|
@@ -1247,7 +1300,7 @@ end)
|
|
|
1247
1300
|
if (path.relative(src, f).split(/[/\\]/).length === 1) continue;
|
|
1248
1301
|
middle.push(f);
|
|
1249
1302
|
}
|
|
1250
|
-
if (opts.dev) {
|
|
1303
|
+
if (opts.dev && !opts.cdn) {
|
|
1251
1304
|
const fwDevUi = path.join(PULSE_DEV_DIR, "ui");
|
|
1252
1305
|
if (fs.existsSync(fwDevUi)) {
|
|
1253
1306
|
for (const f of rglob(fwDevUi, [".lua", ".rblua", ".ts"]).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
|
|
@@ -1288,13 +1341,39 @@ end)
|
|
|
1288
1341
|
parts.push("\n");
|
|
1289
1342
|
if (hasTs) {
|
|
1290
1343
|
const base = `${CDN_BASE_URL}/v${RB_VERSION}`;
|
|
1344
|
+
const lc = this.readLayoutConfig();
|
|
1345
|
+
const luaBool = (v) => v ? "true" : "false";
|
|
1291
1346
|
parts.push(`-- Pulse v${RB_VERSION}
|
|
1347
|
+
`);
|
|
1348
|
+
parts.push(`local _PULSE_LAYOUT={`);
|
|
1349
|
+
parts.push(`title=${JSON.stringify(lc["title"])},`);
|
|
1350
|
+
parts.push(`author=${JSON.stringify(lc["author"])},`);
|
|
1351
|
+
parts.push(`toggleKey=${JSON.stringify(lc["toggleKey"])},`);
|
|
1352
|
+
parts.push(`sizeW=${lc["sizeW"]},sizeH=${lc["sizeH"]},`);
|
|
1353
|
+
parts.push(`theme=${JSON.stringify(lc["theme"])},`);
|
|
1354
|
+
parts.push(`icon=${JSON.stringify(lc["icon"])},`);
|
|
1355
|
+
parts.push(`folder=${JSON.stringify(lc["folder"])},`);
|
|
1356
|
+
parts.push(`acrylic=${luaBool(lc["acrylic"])},`);
|
|
1357
|
+
parts.push(`transparency=${lc["transparency"]},`);
|
|
1358
|
+
parts.push(`openButtonMobileOnly=${luaBool(lc["openButtonMobileOnly"])},`);
|
|
1359
|
+
parts.push(`openButtonIcon=${JSON.stringify(lc["openButtonIcon"])}`);
|
|
1360
|
+
if (lc["description"]) parts.push(`,description=${JSON.stringify(lc["description"])}`);
|
|
1361
|
+
if (lc["discord"]) parts.push(`,discord=${JSON.stringify(lc["discord"])}`);
|
|
1362
|
+
if (lc["version"]) parts.push(`,version=${JSON.stringify(lc["version"])}`);
|
|
1363
|
+
if (opts.dev) parts.push(`,dev=true`);
|
|
1364
|
+
parts.push(`}
|
|
1292
1365
|
`);
|
|
1293
1366
|
parts.push(`local _P=loadstring(game:HttpGet("${base}/bundle.lua"))()
|
|
1294
1367
|
`);
|
|
1368
|
+
if (opts.dev) {
|
|
1369
|
+
parts.push(`_PULSE_DEV = true
|
|
1370
|
+
`);
|
|
1371
|
+
parts.push(`if _P.Pulse and _P.Pulse.Log then _P.Pulse.Log.enable() end
|
|
1372
|
+
`);
|
|
1373
|
+
}
|
|
1295
1374
|
parts.push(`for _k,_v in pairs(_P) do _G[_k]=_v end
|
|
1296
1375
|
`);
|
|
1297
|
-
parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P)
|
|
1376
|
+
parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P,_PULSE_LAYOUT)
|
|
1298
1377
|
`);
|
|
1299
1378
|
parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
|
|
1300
1379
|
`);
|
|
@@ -1304,17 +1383,6 @@ end)
|
|
|
1304
1383
|
`);
|
|
1305
1384
|
parts.push("\n");
|
|
1306
1385
|
}
|
|
1307
|
-
if (opts.dev && fs.existsSync(PULSE_DEV_DIR)) {
|
|
1308
|
-
const rbRoot = path.join(__dirname, "..", "..");
|
|
1309
|
-
for (const f of fs.readdirSync(PULSE_DEV_DIR).filter((n) => n.endsWith(".lua")).sort()) {
|
|
1310
|
-
const full = path.join(PULSE_DEV_DIR, f);
|
|
1311
|
-
const rel = path.relative(rbRoot, full).replace(/\\/g, "/");
|
|
1312
|
-
parts.push(`-- [${rel}]
|
|
1313
|
-
`);
|
|
1314
|
-
parts.push(fs.readFileSync(full, "utf8"));
|
|
1315
|
-
parts.push("\n");
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
1386
|
const label = (p) => {
|
|
1319
1387
|
try {
|
|
1320
1388
|
return path.relative(this.root, p).replace(/\\/g, "/");
|
|
@@ -1387,6 +1455,11 @@ end)
|
|
|
1387
1455
|
parts.push(DEFAULTS_RUNNER);
|
|
1388
1456
|
parts.push("\n");
|
|
1389
1457
|
}
|
|
1458
|
+
if (opts.dev) {
|
|
1459
|
+
parts.push(
|
|
1460
|
+
'-- [dev: log config]\nif Pulse and Pulse.Log then\n Pulse.Log.configure({ level = "debug", console = true })\n Pulse.Log.info("dev", "dev build active")\nend\n\n'
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1390
1463
|
parts.push("-- [generated: destroy registration]\n");
|
|
1391
1464
|
parts.push(DESTROY_REGISTRATION);
|
|
1392
1465
|
parts.push("\n");
|
|
@@ -1797,25 +1870,20 @@ function makeClaudeMd(name) {
|
|
|
1797
1870
|
function makeAgentsMd(name) {
|
|
1798
1871
|
return read("AGENTS.md").replace(/{NAME}/g, name);
|
|
1799
1872
|
}
|
|
1800
|
-
var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES,
|
|
1873
|
+
var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES, MODULE_BOILERPLATE, REMOTE_BOILERPLATE, TEMPLATE_GITIGNORE, TEMPLATE_DEPLOY_EXAMPLE, TEMPLATE_LAYOUT_TS, TEMPLATE_PAGE_HOME_TS, TEMPLATE_COMPONENT_TS, TEMPLATE_EX_SPEED_TS, TEMPLATE_EX_FOV_TS, TEMPLATE_EX_ESP_TS;
|
|
1801
1874
|
var init_templates = __esm({
|
|
1802
1875
|
"src/templates.ts"() {
|
|
1803
1876
|
init_cjs_shims();
|
|
1804
1877
|
DIR = path.join(__dirname, "..", "templates");
|
|
1805
1878
|
TEMPLATE_GLOBALS = read("globals.lua");
|
|
1806
1879
|
TEMPLATE_REMOTES = read("remotes.lua");
|
|
1807
|
-
TEMPLATE_GITIGNORE = read("gitignore");
|
|
1808
|
-
read("layout.rblua");
|
|
1809
|
-
read("page_home.rblua");
|
|
1810
|
-
read("example_speed.rblua");
|
|
1811
|
-
read("example_fov.rblua");
|
|
1812
|
-
read("example_esp.rblua");
|
|
1813
|
-
TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
|
|
1814
1880
|
MODULE_BOILERPLATE = read("module.lua");
|
|
1815
|
-
MODULE_RBLUA_BOILERPLATE = read("module.rblua");
|
|
1816
1881
|
REMOTE_BOILERPLATE = read("remote.lua");
|
|
1882
|
+
TEMPLATE_GITIGNORE = read("gitignore");
|
|
1883
|
+
TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
|
|
1817
1884
|
TEMPLATE_LAYOUT_TS = read("layout.ts");
|
|
1818
1885
|
TEMPLATE_PAGE_HOME_TS = read("page_home.ts");
|
|
1886
|
+
TEMPLATE_COMPONENT_TS = read("component.ts");
|
|
1819
1887
|
TEMPLATE_EX_SPEED_TS = read("component_speed.ts");
|
|
1820
1888
|
TEMPLATE_EX_FOV_TS = read("component_fov.ts");
|
|
1821
1889
|
TEMPLATE_EX_ESP_TS = read("component_esp.ts");
|
|
@@ -2015,9 +2083,8 @@ function toCamel(name) {
|
|
|
2015
2083
|
function cmdNew(args) {
|
|
2016
2084
|
const root = requireProjectRoot();
|
|
2017
2085
|
if (args.type === "module") {
|
|
2018
|
-
const plain = args.plain ?? false;
|
|
2019
2086
|
let relPath = args.path;
|
|
2020
|
-
if (plain) {
|
|
2087
|
+
if (args.plain) {
|
|
2021
2088
|
if (!relPath.endsWith(".lua")) relPath += ".lua";
|
|
2022
2089
|
const dest = pathe.join(root, "src", relPath);
|
|
2023
2090
|
const camel = toCamel(relPath);
|
|
@@ -2033,23 +2100,23 @@ function cmdNew(args) {
|
|
|
2033
2100
|
pOk(`src/${relPath}`);
|
|
2034
2101
|
console.log();
|
|
2035
2102
|
pKv("File", `src/${relPath}`);
|
|
2036
|
-
pKv("Tip", "add
|
|
2103
|
+
pKv("Tip", "add helpers onto func; keep state local");
|
|
2037
2104
|
} else {
|
|
2038
|
-
if (!relPath.endsWith(".
|
|
2105
|
+
if (!relPath.endsWith(".ts")) relPath += ".ts";
|
|
2039
2106
|
const dest = pathe.join(root, "src", relPath);
|
|
2040
2107
|
const camel = toCamel(relPath);
|
|
2108
|
+
const content = TEMPLATE_COMPONENT_TS.replace(/{Camel}/g, camel);
|
|
2041
2109
|
if (fs.existsSync(dest)) {
|
|
2042
2110
|
pFail(`src/${relPath} already exists`);
|
|
2043
2111
|
process.exit(1);
|
|
2044
2112
|
}
|
|
2045
2113
|
fs.mkdirSync(pathe.join(dest, ".."), { recursive: true });
|
|
2046
|
-
fs.writeFileSync(dest,
|
|
2114
|
+
fs.writeFileSync(dest, content, "utf8");
|
|
2047
2115
|
pOk(`src/${relPath}`);
|
|
2048
2116
|
console.log();
|
|
2049
2117
|
pKv("Component", camel);
|
|
2050
|
-
pKv("
|
|
2051
|
-
pKv("
|
|
2052
|
-
pKv("Listen", `Components.${camel}.onEnable:connect(...)`);
|
|
2118
|
+
pKv("Mount", `groupbox('left', '${camel}', { mount: '${camel}' })`);
|
|
2119
|
+
pKv("Tip", "add to a page groupbox in src/pages/*.ts to render it");
|
|
2053
2120
|
}
|
|
2054
2121
|
console.log();
|
|
2055
2122
|
} else if (args.type === "remote") {
|
|
@@ -2067,14 +2134,14 @@ function cmdNew(args) {
|
|
|
2067
2134
|
pOk(`func.Remote_${remoteName}(...) ${gray("\u2192 remotes.lua")}`);
|
|
2068
2135
|
console.log();
|
|
2069
2136
|
} else {
|
|
2070
|
-
pFail(`Unknown type '${args.type}'`, "use: rb new module | rb new remote");
|
|
2137
|
+
pFail(`Unknown type '${args.type}'`, "use: rb new module | rb new module --plain | rb new remote");
|
|
2071
2138
|
process.exit(1);
|
|
2072
2139
|
}
|
|
2073
2140
|
}
|
|
2074
2141
|
async function cmdRemove(args) {
|
|
2075
2142
|
const root = requireProjectRoot();
|
|
2076
2143
|
let relPath = args.path;
|
|
2077
|
-
if (!relPath.endsWith(".lua") && !relPath.endsWith(".
|
|
2144
|
+
if (!relPath.endsWith(".lua") && !relPath.endsWith(".ts")) relPath += ".ts";
|
|
2078
2145
|
const target = pathe.join(root, "src", relPath);
|
|
2079
2146
|
if (!fs.existsSync(target)) {
|
|
2080
2147
|
pFail(`src/${relPath} not found`);
|
|
@@ -2931,19 +2998,12 @@ async function cmdPublish(_args) {
|
|
|
2931
2998
|
const uploads = [
|
|
2932
2999
|
{ remote: `${VERSION_PATH}/bundle.lua`, content: bundleContent }
|
|
2933
3000
|
];
|
|
2934
|
-
const pulseUiDir = pathe.join(PULSE_DIR2, "ui");
|
|
2935
3001
|
for (const adapter of ["windui"]) {
|
|
2936
3002
|
const p = pathe.join(ADAPTERS, `${adapter}.lua`);
|
|
2937
3003
|
if (!fs.existsSync(p)) continue;
|
|
2938
|
-
const parts = [fs.readFileSync(p, "utf8").trimEnd()];
|
|
2939
|
-
const settings = pathe.join(pulseUiDir, `${adapter}_settings.lua`);
|
|
2940
|
-
if (fs.existsSync(settings)) {
|
|
2941
|
-
parts.push(`-- ${adapter}_settings`);
|
|
2942
|
-
parts.push(fs.readFileSync(settings, "utf8").trimEnd());
|
|
2943
|
-
}
|
|
2944
3004
|
uploads.push({
|
|
2945
3005
|
remote: `${VERSION_PATH}/adapters/${adapter}.lua`,
|
|
2946
|
-
content: Buffer.from(
|
|
3006
|
+
content: Buffer.from(fs.readFileSync(p, "utf8"), "utf8")
|
|
2947
3007
|
});
|
|
2948
3008
|
}
|
|
2949
3009
|
pSection(`Uploading to R2 ${gray("(v" + RB_VERSION + " \xB7 " + uploads.length + " files)")}`);
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-rb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "rb CLI — Pulse framework build tool for Roblox script projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rb": "./bin/rb.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "./dist/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"test": "vitest",
|
|
13
|
+
"prepublishOnly": "pnpm run build"
|
|
14
|
+
},
|
|
9
15
|
"files": [
|
|
10
16
|
"dist",
|
|
11
17
|
"bin",
|
|
@@ -33,6 +39,8 @@
|
|
|
33
39
|
"zod": "^4.4.3"
|
|
34
40
|
},
|
|
35
41
|
"devDependencies": {
|
|
42
|
+
"@rb-pulse/core": "workspace:*",
|
|
43
|
+
"@rbxts/types": "^1.0.924",
|
|
36
44
|
"@types/fs-extra": "^11.0.4",
|
|
37
45
|
"@types/luaparse": "^0.2.9",
|
|
38
46
|
"@types/node": "^22.0.0",
|
|
@@ -51,10 +59,5 @@
|
|
|
51
59
|
"pulse",
|
|
52
60
|
"script"
|
|
53
61
|
],
|
|
54
|
-
"license": "MIT"
|
|
55
|
-
|
|
56
|
-
"build": "tsup",
|
|
57
|
-
"dev": "tsx src/index.ts",
|
|
58
|
-
"test": "vitest"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
62
|
+
"license": "MIT"
|
|
63
|
+
}
|
package/pulse/runtime.lua
CHANGED
|
@@ -364,13 +364,16 @@ end
|
|
|
364
364
|
local function _computed(fn) return Computed(fn) end
|
|
365
365
|
|
|
366
366
|
local function _widgetBase(t)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
-- TSTL compiles TypeScript `.bind(sig)` as Lua dot-call: `t.bind(sig)`.
|
|
368
|
+
-- Colon-style methods receive the wrong args (sig becomes self, actual sig is nil).
|
|
369
|
+
-- Close over t so the function works correctly regardless of call style.
|
|
370
|
+
t.bind = function(sig)
|
|
371
|
+
t.signal = sig
|
|
372
|
+
t.id = sig and (sig._id or "?") or nil
|
|
373
|
+
return t
|
|
371
374
|
end
|
|
372
|
-
|
|
373
|
-
|
|
375
|
+
t.withTip = function(text) t.tip = text; return t end
|
|
376
|
+
t.withDefault = function(v) t.default = v; return t end
|
|
374
377
|
return t
|
|
375
378
|
end
|
|
376
379
|
|
|
@@ -429,8 +432,17 @@ local function _hbBind(svc, optsOrFn, fn)
|
|
|
429
432
|
local t=type(every)=="table" and every() or every
|
|
430
433
|
local now=tick(); if (now-last)<t then return end; last=now
|
|
431
434
|
end
|
|
435
|
+
-- Throttled trace: confirms heartbeat is running when condition passes.
|
|
436
|
+
if Pulse and Pulse.Log then
|
|
437
|
+
Pulse.Log.throttle(comp._name, 5, "debug", "tick (condition passed)")
|
|
438
|
+
end
|
|
432
439
|
local ok,err=pcall(cb,a,b)
|
|
433
|
-
if not ok then
|
|
440
|
+
if not ok then
|
|
441
|
+
warn("[Pulse] "..comp._name.." event error: "..tostring(err))
|
|
442
|
+
if Pulse and Pulse.Log then
|
|
443
|
+
Pulse.Log.error(comp._name, "tick error: "..tostring(err))
|
|
444
|
+
end
|
|
445
|
+
end
|
|
434
446
|
end))
|
|
435
447
|
end
|
|
436
448
|
|
|
@@ -443,7 +455,15 @@ _on.inputEnded = function(fn) local c=_needComp("inputEnded"); c
|
|
|
443
455
|
_on.characterAdded = function(fn) local c=_needComp("characterAdded"); c:bind("ca_".._uid(), _LocalPlayer.CharacterAdded:Connect(fn)) end
|
|
444
456
|
_on.characterRemoving = function(fn) local c=_needComp("characterRemoving"); c:bind("cr_".._uid(), _LocalPlayer.CharacterRemoving:Connect(fn)) end
|
|
445
457
|
_on.respawn = function(fn) _needComp("respawn"):onRespawn(fn) end
|
|
446
|
-
_on.signal = function(sig,fn)
|
|
458
|
+
_on.signal = function(sig,fn)
|
|
459
|
+
local comp = _needComp("signal")
|
|
460
|
+
comp:watch(sig, function(v)
|
|
461
|
+
if Pulse and Pulse.Log then
|
|
462
|
+
Pulse.Log.debug(comp._name, "signal "..tostring(sig._id or "?").." → "..tostring(v))
|
|
463
|
+
end
|
|
464
|
+
fn(v)
|
|
465
|
+
end)
|
|
466
|
+
end
|
|
447
467
|
_on.after = function(s,fn) _needComp("after"):task(s,fn) end
|
|
448
468
|
|
|
449
469
|
local function _defineComponent(name, setup)
|
package/templates/globals.lua
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
-- ── Script Config ─────────────────────────────────────────────────────────────
|
|
2
|
-
-- Edit these values. SCRIPT_NAME is used as the UI window title
|
|
2
|
+
-- Edit these values. SCRIPT_NAME is used as the UI window title.
|
|
3
3
|
|
|
4
4
|
local SCRIPT_NAME = "{NAME}"
|
|
5
5
|
local SCRIPT_VERSION = "1.0.0"
|
|
6
6
|
local SCRIPT_AUTHOR = "you"
|
|
7
|
-
local SCRIPT_DISCORD = "" -- optional: discord invite link
|
|
7
|
+
local SCRIPT_DISCORD = "" -- optional: discord invite link
|
|
8
8
|
|
|
9
9
|
-- ── Shared helpers ─────────────────────────────────────────────────────────────
|
|
10
|
-
-- Add
|
|
11
|
-
--
|
|
10
|
+
-- Add game-specific shared helpers here. Pulse runtime helpers (_PulseGet*)
|
|
11
|
+
-- and all TypeScript component globals are injected automatically.
|
|
12
12
|
|
|
13
13
|
local func = {}
|
|
14
14
|
|
|
15
15
|
-- ── Players ────────────────────────────────────────────────────────────────────
|
|
16
|
-
-- Event-driven cache
|
|
16
|
+
-- Event-driven player cache — stays up to date as players join/leave.
|
|
17
17
|
local _playerSet = {}
|
|
18
18
|
local _PS = game:GetService("Players")
|
|
19
19
|
local _LP = _PS.LocalPlayer
|
|
20
20
|
|
|
21
21
|
for _, p in ipairs(_PS:GetPlayers()) do _playerSet[p] = true end
|
|
22
|
-
_PS.PlayerAdded:Connect(function(p)
|
|
23
|
-
|
|
24
|
-
local n = 0; for _ in pairs(_playerSet) do n = n + 1 end
|
|
25
|
-
Pulse.Monitor.set("players", n)
|
|
26
|
-
end)
|
|
27
|
-
_PS.PlayerRemoving:Connect(function(p)
|
|
28
|
-
_playerSet[p] = nil
|
|
29
|
-
local n = 0; for _ in pairs(_playerSet) do n = n + 1 end
|
|
30
|
-
Pulse.Monitor.set("players", n)
|
|
31
|
-
end)
|
|
22
|
+
_PS.PlayerAdded:Connect(function(p) _playerSet[p] = true end)
|
|
23
|
+
_PS.PlayerRemoving:Connect(function(p) _playerSet[p] = nil end)
|
|
32
24
|
|
|
33
25
|
func.GetCachedPlayers = function()
|
|
34
26
|
local t = {}
|
|
@@ -36,24 +28,16 @@ func.GetCachedPlayers = function()
|
|
|
36
28
|
return t
|
|
37
29
|
end
|
|
38
30
|
|
|
39
|
-
-- ── Team monitor ──────────────────────────────────────────────────────────────
|
|
40
|
-
-- Tracks the local player's team in the dev overlay.
|
|
41
|
-
Pulse.Monitor.set("team", _LP.Team and _LP.Team.Name or "none")
|
|
42
|
-
_LP:GetPropertyChangedSignal("Team"):Connect(function()
|
|
43
|
-
Pulse.Monitor.set("team", _LP.Team and _LP.Team.Name or "none")
|
|
44
|
-
end)
|
|
45
|
-
|
|
46
31
|
-- ── Entity folder watcher ──────────────────────────────────────────────────────
|
|
47
|
-
-- Use _watchFolder to keep a set live as
|
|
48
|
-
--
|
|
32
|
+
-- Use _watchFolder to keep a set live as instances spawn/despawn.
|
|
33
|
+
-- Uncomment and adapt to your game's folder structure.
|
|
49
34
|
local function _watchFolder(folder, set)
|
|
50
35
|
for _, child in ipairs(folder:GetChildren()) do set[child] = true end
|
|
51
36
|
folder.ChildAdded:Connect(function(c) set[c] = true end)
|
|
52
37
|
folder.ChildRemoved:Connect(function(c) set[c] = nil end)
|
|
53
38
|
end
|
|
54
39
|
|
|
55
|
-
-- Example
|
|
56
|
-
--
|
|
40
|
+
-- Example:
|
|
57
41
|
-- local _enemySet = {}
|
|
58
42
|
-- task.spawn(function()
|
|
59
43
|
-- local folder = workspace:WaitForChild("Enemies", 30)
|
package/templates/layout.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// Pulse layout — configures the script window.
|
|
2
2
|
// Based on Next.js root layout convention.
|
|
3
|
-
// uiLibrary: 'windui' | 'linoria'
|
|
4
3
|
|
|
5
4
|
export default {
|
|
6
5
|
title: '{NAME}',
|
|
7
6
|
author: '',
|
|
8
7
|
toggleKey: 'RightControl',
|
|
9
8
|
size: [850, 560] as [number, number],
|
|
10
|
-
uiLibrary: 'windui' as
|
|
9
|
+
uiLibrary: 'windui' as const,
|
|
11
10
|
theme: 'Indigo',
|
|
12
11
|
icon: 'code-2',
|
|
13
12
|
folder: '{NAME}',
|
package/templates/page_home.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
// Home page tab — Next.js-style file-based routing.
|
|
2
|
-
// Filename prefix (1_) determines tab order.
|
|
3
|
-
|
|
4
1
|
definePage('Home', { icon: 'house' }, () => [
|
|
5
|
-
groupbox('left', 'Player', { icon: 'person',
|
|
6
|
-
groupbox('left', 'Player', { icon: 'person',
|
|
7
|
-
groupbox('right', 'Visuals', { icon: 'eye',
|
|
2
|
+
groupbox('left', 'Player', { icon: 'person', mount: 'SpeedHack' }),
|
|
3
|
+
groupbox('left', 'Player', { icon: 'person', mount: 'FOVChanger' }),
|
|
4
|
+
groupbox('right', 'Visuals', { icon: 'eye', mount: 'PlayerESP' }),
|
|
8
5
|
])
|