@trops/dash-core 0.1.205 → 0.1.206
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/electron/index.js +1882 -1875
- package/dist/electron/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -29580,1790 +29580,1785 @@ var dashboardConfigUtils$1 = {
|
|
|
29580
29580
|
checkApiCompatibility,
|
|
29581
29581
|
};
|
|
29582
29582
|
|
|
29583
|
-
var widgetRegistry$1 = {exports: {}};
|
|
29584
|
-
|
|
29585
|
-
var dynamicWidgetLoader$2 = {exports: {}};
|
|
29586
|
-
|
|
29587
29583
|
/**
|
|
29588
|
-
*
|
|
29584
|
+
* registryAuthController.js
|
|
29589
29585
|
*
|
|
29590
|
-
*
|
|
29591
|
-
*
|
|
29592
|
-
* widgetBundleLoader.js eval pipeline (new Function() + require shim).
|
|
29586
|
+
* Manages authentication with the Dash registry service.
|
|
29587
|
+
* Uses OAuth device code flow for desktop app authentication.
|
|
29593
29588
|
*
|
|
29594
|
-
*
|
|
29589
|
+
* Flow:
|
|
29590
|
+
* 1. App calls initiateDeviceFlow() — gets device code + verification URL
|
|
29591
|
+
* 2. User opens verification URL in browser, signs in, enters code
|
|
29592
|
+
* 3. App polls pollForToken() until authorized
|
|
29593
|
+
* 4. Token stored securely via electron-store (encrypted)
|
|
29595
29594
|
*/
|
|
29596
29595
|
|
|
29597
|
-
const
|
|
29598
|
-
|
|
29596
|
+
const REGISTRY_BASE_URL$1 =
|
|
29597
|
+
process.env.DASH_REGISTRY_API_URL ||
|
|
29598
|
+
"https://main.d919rwhuzp7rj.amplifyapp.com";
|
|
29599
|
+
|
|
29600
|
+
// Lazy-load electron-store to avoid issues when not installed
|
|
29601
|
+
let store$3 = null;
|
|
29602
|
+
function getStore$1() {
|
|
29603
|
+
if (!store$3) {
|
|
29604
|
+
const Store = require$$1$1;
|
|
29605
|
+
store$3 = new Store({
|
|
29606
|
+
name: "dash-registry-auth",
|
|
29607
|
+
encryptionKey: "dash-registry-v1",
|
|
29608
|
+
});
|
|
29609
|
+
}
|
|
29610
|
+
return store$3;
|
|
29611
|
+
}
|
|
29599
29612
|
|
|
29600
29613
|
/**
|
|
29601
|
-
*
|
|
29602
|
-
*
|
|
29603
|
-
* ZIP extraction can create a nested structure like:
|
|
29604
|
-
* Weather/weather-widget/widgets/ instead of Weather/widgets/
|
|
29614
|
+
* Initiate the OAuth device code flow.
|
|
29615
|
+
* Returns the device code, user code, and verification URL.
|
|
29605
29616
|
*
|
|
29606
|
-
*
|
|
29607
|
-
|
|
29617
|
+
* @returns {Promise<Object>} { deviceCode, userCode, verificationUrl, verificationUrlComplete, expiresIn, interval }
|
|
29618
|
+
*/
|
|
29619
|
+
async function initiateDeviceFlow$1() {
|
|
29620
|
+
const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/device`, {
|
|
29621
|
+
method: "POST",
|
|
29622
|
+
headers: { "Content-Type": "application/json" },
|
|
29623
|
+
});
|
|
29624
|
+
|
|
29625
|
+
if (!response.ok) {
|
|
29626
|
+
throw new Error(`Device flow initiation failed: ${response.status}`);
|
|
29627
|
+
}
|
|
29628
|
+
|
|
29629
|
+
const data = await response.json();
|
|
29630
|
+
|
|
29631
|
+
return {
|
|
29632
|
+
deviceCode: data.device_code,
|
|
29633
|
+
userCode: data.user_code,
|
|
29634
|
+
verificationUrl: data.verification_uri,
|
|
29635
|
+
verificationUrlComplete: data.verification_uri_complete,
|
|
29636
|
+
expiresIn: data.expires_in,
|
|
29637
|
+
interval: data.interval,
|
|
29638
|
+
};
|
|
29639
|
+
}
|
|
29640
|
+
|
|
29641
|
+
/**
|
|
29642
|
+
* Poll the registry for token after user completes browser auth.
|
|
29608
29643
|
*
|
|
29609
|
-
* @param {string}
|
|
29610
|
-
* @returns {
|
|
29644
|
+
* @param {string} deviceCode - The device code from initiateDeviceFlow()
|
|
29645
|
+
* @returns {Promise<Object>} { status: 'pending' | 'authorized' | 'expired', token?, userId? }
|
|
29611
29646
|
*/
|
|
29612
|
-
function
|
|
29613
|
-
const
|
|
29614
|
-
|
|
29615
|
-
|
|
29647
|
+
async function pollForToken$1(deviceCode) {
|
|
29648
|
+
const response = await fetch(
|
|
29649
|
+
`${REGISTRY_BASE_URL$1}/api/auth/device?device_code=${encodeURIComponent(deviceCode)}`,
|
|
29650
|
+
);
|
|
29651
|
+
|
|
29652
|
+
if (response.status === 428) {
|
|
29653
|
+
return { status: "pending" };
|
|
29616
29654
|
}
|
|
29617
29655
|
|
|
29618
|
-
|
|
29619
|
-
|
|
29620
|
-
|
|
29621
|
-
|
|
29656
|
+
if (response.status === 400) {
|
|
29657
|
+
const data = await response.json();
|
|
29658
|
+
if (data.error === "expired_token") {
|
|
29659
|
+
return { status: "expired" };
|
|
29660
|
+
}
|
|
29661
|
+
return { status: "pending" };
|
|
29622
29662
|
}
|
|
29623
29663
|
|
|
29624
|
-
|
|
29625
|
-
|
|
29626
|
-
const entries = fs$2.readdirSync(widgetPath, { withFileTypes: true });
|
|
29627
|
-
const subdirs = entries.filter(
|
|
29628
|
-
(e) =>
|
|
29629
|
-
e.isDirectory() &&
|
|
29630
|
-
!e.name.startsWith(".") &&
|
|
29631
|
-
e.name !== "dist" &&
|
|
29632
|
-
e.name !== "node_modules",
|
|
29633
|
-
);
|
|
29664
|
+
if (response.ok) {
|
|
29665
|
+
const data = await response.json();
|
|
29634
29666
|
|
|
29635
|
-
|
|
29636
|
-
|
|
29637
|
-
|
|
29638
|
-
|
|
29639
|
-
|
|
29640
|
-
|
|
29641
|
-
|
|
29642
|
-
|
|
29643
|
-
|
|
29667
|
+
// Store the token securely
|
|
29668
|
+
const s = getStore$1();
|
|
29669
|
+
s.set("accessToken", data.access_token);
|
|
29670
|
+
s.set("userId", data.user_id);
|
|
29671
|
+
s.set("tokenType", data.token_type);
|
|
29672
|
+
s.set("authenticatedAt", new Date().toISOString());
|
|
29673
|
+
|
|
29674
|
+
return {
|
|
29675
|
+
status: "authorized",
|
|
29676
|
+
token: data.access_token,
|
|
29677
|
+
userId: data.user_id,
|
|
29678
|
+
};
|
|
29644
29679
|
}
|
|
29645
29680
|
|
|
29646
|
-
|
|
29681
|
+
throw new Error(`Unexpected response: ${response.status}`);
|
|
29647
29682
|
}
|
|
29648
29683
|
|
|
29649
29684
|
/**
|
|
29650
|
-
*
|
|
29651
|
-
*
|
|
29652
|
-
* For each {Name}.dash.js found in the widgets/ directory, a synthetic
|
|
29653
|
-
* entry point is generated that imports the component + config and
|
|
29654
|
-
* re-exports them as `{ ...config, component }` — matching what
|
|
29655
|
-
* extractWidgetConfigs() in widgetBundleLoader.js expects.
|
|
29685
|
+
* Get the stored auth token.
|
|
29656
29686
|
*
|
|
29657
|
-
* @
|
|
29658
|
-
* @returns {Promise<string|null>} Path to the compiled bundle, or null if nothing to compile
|
|
29687
|
+
* @returns {Object|null} { token, userId, authenticatedAt } or null if not authenticated
|
|
29659
29688
|
*/
|
|
29660
|
-
|
|
29661
|
-
|
|
29689
|
+
function getStoredToken$3() {
|
|
29690
|
+
try {
|
|
29691
|
+
const s = getStore$1();
|
|
29692
|
+
const token = s.get("accessToken");
|
|
29693
|
+
if (!token) return null;
|
|
29662
29694
|
|
|
29663
|
-
|
|
29664
|
-
|
|
29665
|
-
|
|
29666
|
-
|
|
29695
|
+
return {
|
|
29696
|
+
token,
|
|
29697
|
+
userId: s.get("userId"),
|
|
29698
|
+
authenticatedAt: s.get("authenticatedAt"),
|
|
29699
|
+
};
|
|
29700
|
+
} catch {
|
|
29667
29701
|
return null;
|
|
29668
29702
|
}
|
|
29703
|
+
}
|
|
29669
29704
|
|
|
29670
|
-
|
|
29671
|
-
|
|
29672
|
-
|
|
29673
|
-
|
|
29674
|
-
|
|
29675
|
-
|
|
29676
|
-
|
|
29677
|
-
|
|
29678
|
-
return
|
|
29705
|
+
/**
|
|
29706
|
+
* Check if the user is authenticated with the registry.
|
|
29707
|
+
*
|
|
29708
|
+
* @returns {Object} { authenticated: boolean, userId?: string }
|
|
29709
|
+
*/
|
|
29710
|
+
function getAuthStatus$1() {
|
|
29711
|
+
const stored = getStoredToken$3();
|
|
29712
|
+
if (!stored) {
|
|
29713
|
+
return { authenticated: false };
|
|
29679
29714
|
}
|
|
29680
29715
|
|
|
29681
|
-
|
|
29682
|
-
|
|
29683
|
-
|
|
29684
|
-
|
|
29685
|
-
|
|
29686
|
-
|
|
29687
|
-
const exportParts = [];
|
|
29716
|
+
return {
|
|
29717
|
+
authenticated: true,
|
|
29718
|
+
userId: stored.userId,
|
|
29719
|
+
authenticatedAt: stored.authenticatedAt,
|
|
29720
|
+
};
|
|
29721
|
+
}
|
|
29688
29722
|
|
|
29689
|
-
|
|
29690
|
-
|
|
29691
|
-
|
|
29692
|
-
|
|
29693
|
-
|
|
29723
|
+
/**
|
|
29724
|
+
* Get the user's registry profile.
|
|
29725
|
+
*
|
|
29726
|
+
* @returns {Promise<Object|null>} User profile or null
|
|
29727
|
+
*/
|
|
29728
|
+
async function getRegistryProfile$2() {
|
|
29729
|
+
const stored = getStoredToken$3();
|
|
29730
|
+
if (!stored) return null;
|
|
29694
29731
|
|
|
29695
|
-
|
|
29696
|
-
|
|
29697
|
-
|
|
29698
|
-
|
|
29732
|
+
try {
|
|
29733
|
+
const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me`, {
|
|
29734
|
+
headers: {
|
|
29735
|
+
Authorization: `Bearer ${stored.token}`,
|
|
29736
|
+
},
|
|
29737
|
+
});
|
|
29699
29738
|
|
|
29700
|
-
if (
|
|
29701
|
-
//
|
|
29702
|
-
|
|
29703
|
-
|
|
29704
|
-
);
|
|
29705
|
-
exportParts.push(
|
|
29706
|
-
`export const ${componentName} = { ...${componentName}Config, component: ${componentName}Comp };`,
|
|
29707
|
-
);
|
|
29708
|
-
} else {
|
|
29709
|
-
// Config-only (no component source file)
|
|
29710
|
-
exportParts.push(
|
|
29711
|
-
`export const ${componentName} = ${componentName}Config;`,
|
|
29712
|
-
);
|
|
29739
|
+
if (response.status === 401) {
|
|
29740
|
+
// Token expired or invalid — clear stored credentials
|
|
29741
|
+
clearToken();
|
|
29742
|
+
return null;
|
|
29713
29743
|
}
|
|
29744
|
+
if (!response.ok) return null;
|
|
29745
|
+
|
|
29746
|
+
const data = await response.json();
|
|
29747
|
+
return data.user || null;
|
|
29748
|
+
} catch {
|
|
29749
|
+
return null;
|
|
29714
29750
|
}
|
|
29751
|
+
}
|
|
29715
29752
|
|
|
29716
|
-
|
|
29753
|
+
/**
|
|
29754
|
+
* Clear stored auth token (logout).
|
|
29755
|
+
*/
|
|
29756
|
+
function clearToken() {
|
|
29757
|
+
try {
|
|
29758
|
+
const s = getStore$1();
|
|
29759
|
+
s.clear();
|
|
29760
|
+
console.log("[RegistryAuthController] Token cleared");
|
|
29761
|
+
} catch (err) {
|
|
29762
|
+
console.error("[RegistryAuthController] Error clearing token:", err);
|
|
29763
|
+
}
|
|
29764
|
+
}
|
|
29717
29765
|
|
|
29718
|
-
|
|
29719
|
-
|
|
29720
|
-
|
|
29721
|
-
|
|
29766
|
+
/**
|
|
29767
|
+
* Update the authenticated user's registry profile.
|
|
29768
|
+
*
|
|
29769
|
+
* @param {Object} updates - Fields to update (e.g. { displayName })
|
|
29770
|
+
* @returns {Promise<Object|null>} Updated user or null on 401
|
|
29771
|
+
*/
|
|
29772
|
+
async function updateRegistryProfile$1(updates) {
|
|
29773
|
+
const stored = getStoredToken$3();
|
|
29774
|
+
if (!stored) return null;
|
|
29722
29775
|
|
|
29723
29776
|
try {
|
|
29724
|
-
|
|
29725
|
-
|
|
29726
|
-
|
|
29727
|
-
|
|
29777
|
+
const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me`, {
|
|
29778
|
+
method: "PATCH",
|
|
29779
|
+
headers: {
|
|
29780
|
+
Authorization: `Bearer ${stored.token}`,
|
|
29781
|
+
"Content-Type": "application/json",
|
|
29782
|
+
},
|
|
29783
|
+
body: JSON.stringify(updates),
|
|
29784
|
+
});
|
|
29728
29785
|
|
|
29729
|
-
|
|
29786
|
+
if (response.status === 401) {
|
|
29787
|
+
clearToken();
|
|
29788
|
+
return null;
|
|
29789
|
+
}
|
|
29790
|
+
if (!response.ok) return null;
|
|
29730
29791
|
|
|
29731
|
-
|
|
29732
|
-
|
|
29733
|
-
|
|
29792
|
+
const data = await response.json();
|
|
29793
|
+
return data.user || null;
|
|
29794
|
+
} catch {
|
|
29795
|
+
return null;
|
|
29796
|
+
}
|
|
29797
|
+
}
|
|
29734
29798
|
|
|
29735
|
-
|
|
29736
|
-
|
|
29737
|
-
|
|
29799
|
+
/**
|
|
29800
|
+
* Get the authenticated user's published packages.
|
|
29801
|
+
*
|
|
29802
|
+
* @returns {Promise<Object|null>} { packages: [...] } or null
|
|
29803
|
+
*/
|
|
29804
|
+
async function getRegistryPackages$1() {
|
|
29805
|
+
const stored = getStoredToken$3();
|
|
29806
|
+
if (!stored) return null;
|
|
29738
29807
|
|
|
29739
|
-
|
|
29740
|
-
|
|
29741
|
-
|
|
29742
|
-
|
|
29743
|
-
|
|
29744
|
-
// These modules are provided by the host app via MODULE_MAP
|
|
29745
|
-
// in widgetBundleLoader.js — do NOT bundle them
|
|
29746
|
-
external: [
|
|
29747
|
-
"react",
|
|
29748
|
-
"react-dom",
|
|
29749
|
-
"@trops/dash-react",
|
|
29750
|
-
"@trops/dash-core",
|
|
29751
|
-
"react/jsx-runtime",
|
|
29752
|
-
"prop-types",
|
|
29753
|
-
],
|
|
29754
|
-
// Treat .js files as JSX (widget sources use JSX in .js files)
|
|
29755
|
-
loader: { ".js": "jsx" },
|
|
29756
|
-
logLevel: "warning",
|
|
29808
|
+
try {
|
|
29809
|
+
const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me/packages`, {
|
|
29810
|
+
headers: {
|
|
29811
|
+
Authorization: `Bearer ${stored.token}`,
|
|
29812
|
+
},
|
|
29757
29813
|
});
|
|
29758
29814
|
|
|
29759
|
-
|
|
29760
|
-
|
|
29761
|
-
|
|
29762
|
-
console.error(
|
|
29763
|
-
`[WidgetCompiler] Compilation failed for ${widgetPath}:`,
|
|
29764
|
-
error,
|
|
29765
|
-
);
|
|
29766
|
-
throw error;
|
|
29767
|
-
} finally {
|
|
29768
|
-
// Clean up temporary entry file
|
|
29769
|
-
try {
|
|
29770
|
-
if (fs$2.existsSync(entryPath)) {
|
|
29771
|
-
fs$2.unlinkSync(entryPath);
|
|
29772
|
-
}
|
|
29773
|
-
} catch (cleanupError) {
|
|
29774
|
-
// Non-fatal
|
|
29775
|
-
console.warn(
|
|
29776
|
-
`[WidgetCompiler] Could not remove temp entry file:`,
|
|
29777
|
-
cleanupError,
|
|
29778
|
-
);
|
|
29815
|
+
if (response.status === 401) {
|
|
29816
|
+
clearToken();
|
|
29817
|
+
return null;
|
|
29779
29818
|
}
|
|
29819
|
+
if (!response.ok) return null;
|
|
29820
|
+
|
|
29821
|
+
return await response.json();
|
|
29822
|
+
} catch {
|
|
29823
|
+
return null;
|
|
29780
29824
|
}
|
|
29781
29825
|
}
|
|
29782
29826
|
|
|
29783
|
-
var widgetCompiler$1 = { compileWidget, findWidgetsDir: findWidgetsDir$1 };
|
|
29784
|
-
|
|
29785
29827
|
/**
|
|
29786
|
-
*
|
|
29787
|
-
*
|
|
29788
|
-
* Loads React components and configurations from downloaded/local widget paths
|
|
29789
|
-
* Works with widgets that follow the Dash widget structure:
|
|
29790
|
-
* - widgets/
|
|
29791
|
-
* - WidgetName.js (React component)
|
|
29792
|
-
* - WidgetName.dash.js (configuration)
|
|
29828
|
+
* Update a published package's metadata.
|
|
29793
29829
|
*
|
|
29794
|
-
*
|
|
29830
|
+
* @param {string} scope - Package scope (e.g. "@trops")
|
|
29831
|
+
* @param {string} name - Package name
|
|
29832
|
+
* @param {Object} updates - Fields to update (displayName, description, category, tags, visibility)
|
|
29833
|
+
* @returns {Promise<Object|null>} Updated package or null
|
|
29795
29834
|
*/
|
|
29835
|
+
async function updateRegistryPackage$1(scope, name, updates) {
|
|
29836
|
+
const stored = getStoredToken$3();
|
|
29837
|
+
if (!stored) return null;
|
|
29796
29838
|
|
|
29797
|
-
|
|
29798
|
-
const
|
|
29799
|
-
|
|
29800
|
-
|
|
29839
|
+
try {
|
|
29840
|
+
const response = await fetch(
|
|
29841
|
+
`${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
|
|
29842
|
+
{
|
|
29843
|
+
method: "PATCH",
|
|
29844
|
+
headers: {
|
|
29845
|
+
Authorization: `Bearer ${stored.token}`,
|
|
29846
|
+
"Content-Type": "application/json",
|
|
29847
|
+
},
|
|
29848
|
+
body: JSON.stringify(updates),
|
|
29849
|
+
},
|
|
29850
|
+
);
|
|
29801
29851
|
|
|
29802
|
-
|
|
29803
|
-
|
|
29804
|
-
|
|
29805
|
-
|
|
29806
|
-
|
|
29807
|
-
}
|
|
29852
|
+
if (response.status === 401) {
|
|
29853
|
+
clearToken();
|
|
29854
|
+
return null;
|
|
29855
|
+
}
|
|
29856
|
+
if (!response.ok) return null;
|
|
29808
29857
|
|
|
29809
|
-
|
|
29810
|
-
|
|
29811
|
-
|
|
29812
|
-
*/
|
|
29813
|
-
setComponentManager(manager) {
|
|
29814
|
-
this.componentManager = manager;
|
|
29858
|
+
return await response.json();
|
|
29859
|
+
} catch {
|
|
29860
|
+
return null;
|
|
29815
29861
|
}
|
|
29862
|
+
}
|
|
29816
29863
|
|
|
29817
|
-
|
|
29818
|
-
|
|
29819
|
-
|
|
29820
|
-
|
|
29821
|
-
|
|
29822
|
-
|
|
29823
|
-
|
|
29824
|
-
|
|
29825
|
-
|
|
29826
|
-
|
|
29827
|
-
const cacheKey = `${widgetName}:${componentName}`;
|
|
29828
|
-
|
|
29829
|
-
if (this.loadedWidgets.has(cacheKey)) {
|
|
29830
|
-
console.log(`[DynamicWidgetLoader] Loading ${widgetName} from cache`);
|
|
29831
|
-
return this.loadedWidgets.get(cacheKey);
|
|
29832
|
-
}
|
|
29833
|
-
|
|
29834
|
-
console.log(
|
|
29835
|
-
`[DynamicWidgetLoader] Loading widget: ${widgetName} from ${widgetPath}`,
|
|
29836
|
-
);
|
|
29864
|
+
/**
|
|
29865
|
+
* Delete a published package from the registry.
|
|
29866
|
+
*
|
|
29867
|
+
* @param {string} scope - Package scope (e.g. "@trops")
|
|
29868
|
+
* @param {string} name - Package name
|
|
29869
|
+
* @returns {Promise<Object|null>} Response or null
|
|
29870
|
+
*/
|
|
29871
|
+
async function deleteRegistryPackage(scope, name) {
|
|
29872
|
+
const stored = getStoredToken$3();
|
|
29873
|
+
if (!stored) return null;
|
|
29837
29874
|
|
|
29838
|
-
|
|
29839
|
-
|
|
29840
|
-
|
|
29841
|
-
|
|
29875
|
+
try {
|
|
29876
|
+
const response = await fetch(
|
|
29877
|
+
`${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
|
|
29878
|
+
{
|
|
29879
|
+
method: "DELETE",
|
|
29880
|
+
headers: {
|
|
29881
|
+
Authorization: `Bearer ${stored.token}`,
|
|
29882
|
+
},
|
|
29883
|
+
},
|
|
29884
|
+
);
|
|
29842
29885
|
|
|
29843
|
-
|
|
29844
|
-
|
|
29845
|
-
|
|
29846
|
-
|
|
29847
|
-
|
|
29848
|
-
}
|
|
29886
|
+
if (response.status === 401) {
|
|
29887
|
+
clearToken();
|
|
29888
|
+
return null;
|
|
29889
|
+
}
|
|
29890
|
+
if (!response.ok) return null;
|
|
29849
29891
|
|
|
29850
|
-
|
|
29892
|
+
return await response.json();
|
|
29893
|
+
} catch {
|
|
29894
|
+
return null;
|
|
29895
|
+
}
|
|
29896
|
+
}
|
|
29851
29897
|
|
|
29852
|
-
|
|
29853
|
-
|
|
29854
|
-
|
|
29855
|
-
|
|
29898
|
+
var registryAuthController$1 = {
|
|
29899
|
+
initiateDeviceFlow: initiateDeviceFlow$1,
|
|
29900
|
+
pollForToken: pollForToken$1,
|
|
29901
|
+
getStoredToken: getStoredToken$3,
|
|
29902
|
+
getAuthStatus: getAuthStatus$1,
|
|
29903
|
+
getRegistryProfile: getRegistryProfile$2,
|
|
29904
|
+
updateRegistryProfile: updateRegistryProfile$1,
|
|
29905
|
+
getRegistryPackages: getRegistryPackages$1,
|
|
29906
|
+
updateRegistryPackage: updateRegistryPackage$1,
|
|
29907
|
+
deleteRegistryPackage,
|
|
29908
|
+
clearToken,
|
|
29909
|
+
};
|
|
29856
29910
|
|
|
29857
|
-
|
|
29911
|
+
var widgetRegistry$1 = {exports: {}};
|
|
29858
29912
|
|
|
29859
|
-
|
|
29860
|
-
try {
|
|
29861
|
-
// Use scoped id as registration key if available,
|
|
29862
|
-
// otherwise fall back to componentName
|
|
29863
|
-
const registrationKey = config.id || componentName;
|
|
29864
|
-
this.componentManager.registerWidget(config, registrationKey);
|
|
29865
|
-
registered = true;
|
|
29866
|
-
console.log(
|
|
29867
|
-
`[DynamicWidgetLoader] ✓ Registered ${registrationKey} with ComponentManager`,
|
|
29868
|
-
);
|
|
29869
|
-
} catch (regError) {
|
|
29870
|
-
console.warn(
|
|
29871
|
-
`[DynamicWidgetLoader] Failed to register with ComponentManager:`,
|
|
29872
|
-
regError,
|
|
29873
|
-
);
|
|
29874
|
-
}
|
|
29875
|
-
}
|
|
29913
|
+
var dynamicWidgetLoader$2 = {exports: {}};
|
|
29876
29914
|
|
|
29877
|
-
|
|
29878
|
-
|
|
29915
|
+
/**
|
|
29916
|
+
* Widget Compiler
|
|
29917
|
+
*
|
|
29918
|
+
* Compiles raw widget source files (.js + .dash.js) into a single CJS bundle
|
|
29919
|
+
* using esbuild. The output bundle is consumable by the existing
|
|
29920
|
+
* widgetBundleLoader.js eval pipeline (new Function() + require shim).
|
|
29921
|
+
*
|
|
29922
|
+
* Runs in the Electron main process at widget install time.
|
|
29923
|
+
*/
|
|
29879
29924
|
|
|
29880
|
-
|
|
29881
|
-
|
|
29882
|
-
console.error(
|
|
29883
|
-
`[DynamicWidgetLoader] Error loading widget ${widgetName}:`,
|
|
29884
|
-
error,
|
|
29885
|
-
);
|
|
29886
|
-
throw error;
|
|
29887
|
-
}
|
|
29888
|
-
}
|
|
29925
|
+
const fs$2 = require$$0$3;
|
|
29926
|
+
const path$5 = require$$1$2;
|
|
29889
29927
|
|
|
29890
|
-
|
|
29891
|
-
|
|
29892
|
-
|
|
29893
|
-
|
|
29894
|
-
|
|
29895
|
-
|
|
29896
|
-
|
|
29897
|
-
|
|
29898
|
-
|
|
29899
|
-
|
|
29900
|
-
|
|
29901
|
-
|
|
29902
|
-
|
|
29903
|
-
|
|
29904
|
-
|
|
29905
|
-
|
|
29906
|
-
if (varExportMatch) {
|
|
29907
|
-
const varName = varExportMatch[1];
|
|
29908
|
-
const varDeclMatch = source.match(
|
|
29909
|
-
new RegExp(
|
|
29910
|
-
`(?:const|let|var)\\s+${varName}\\s*=\\s*({[\\s\\S]*?});\\s*(?:export\\s+default)`,
|
|
29911
|
-
),
|
|
29912
|
-
);
|
|
29913
|
-
if (varDeclMatch) {
|
|
29914
|
-
exportMatch = varDeclMatch;
|
|
29915
|
-
}
|
|
29916
|
-
}
|
|
29917
|
-
}
|
|
29918
|
-
|
|
29919
|
-
if (!exportMatch) {
|
|
29920
|
-
throw new Error("Could not find default export in config file");
|
|
29921
|
-
}
|
|
29922
|
-
|
|
29923
|
-
// Sanitize component references so vm.runInContext doesn't fail
|
|
29924
|
-
// on unresolvable imports — replace component: SomeName with component: "SomeName"
|
|
29925
|
-
const exportedObjectStr = exportMatch[1].replace(
|
|
29926
|
-
/component\s*:\s*([A-Z][a-zA-Z0-9_$]*)/g,
|
|
29927
|
-
'component: "$1"',
|
|
29928
|
-
);
|
|
29929
|
-
|
|
29930
|
-
const context = vm.createContext({ module: { exports: {} } });
|
|
29931
|
-
vm.runInContext(`module.exports = ${exportedObjectStr}`, context);
|
|
29932
|
-
|
|
29933
|
-
return context.module.exports;
|
|
29934
|
-
} catch (error) {
|
|
29935
|
-
console.error(`[DynamicWidgetLoader] Error loading config:`, error);
|
|
29936
|
-
throw error;
|
|
29937
|
-
}
|
|
29928
|
+
/**
|
|
29929
|
+
* Find the widgets/ directory, handling nested ZIP extraction.
|
|
29930
|
+
*
|
|
29931
|
+
* ZIP extraction can create a nested structure like:
|
|
29932
|
+
* Weather/weather-widget/widgets/ instead of Weather/widgets/
|
|
29933
|
+
*
|
|
29934
|
+
* If widgets/ doesn't exist at root, check one level deeper for a
|
|
29935
|
+
* single subdirectory that contains widgets/.
|
|
29936
|
+
*
|
|
29937
|
+
* @param {string} widgetPath - Absolute path to the widget directory
|
|
29938
|
+
* @returns {string|null} Path to the widgets/ directory, or null
|
|
29939
|
+
*/
|
|
29940
|
+
function findWidgetsDir$1(widgetPath) {
|
|
29941
|
+
const direct = path$5.join(widgetPath, "widgets");
|
|
29942
|
+
if (fs$2.existsSync(direct)) {
|
|
29943
|
+
return direct;
|
|
29938
29944
|
}
|
|
29939
29945
|
|
|
29940
|
-
|
|
29941
|
-
|
|
29942
|
-
|
|
29943
|
-
|
|
29944
|
-
|
|
29945
|
-
discoverWidgets(widgetPath) {
|
|
29946
|
-
try {
|
|
29947
|
-
const widgetsDir = findWidgetsDir(widgetPath);
|
|
29948
|
-
if (!widgetsDir) {
|
|
29949
|
-
return [];
|
|
29950
|
-
}
|
|
29951
|
-
|
|
29952
|
-
const files = fs$1.readdirSync(widgetsDir);
|
|
29953
|
-
const widgets = new Set();
|
|
29946
|
+
// Check configs/ directory (used by packageZip.js for distributed widgets)
|
|
29947
|
+
const configs = path$5.join(widgetPath, "configs");
|
|
29948
|
+
if (fs$2.existsSync(configs)) {
|
|
29949
|
+
return configs;
|
|
29950
|
+
}
|
|
29954
29951
|
|
|
29955
|
-
|
|
29956
|
-
|
|
29957
|
-
|
|
29958
|
-
|
|
29959
|
-
|
|
29960
|
-
|
|
29952
|
+
// Check one level deeper for nested ZIP extraction
|
|
29953
|
+
try {
|
|
29954
|
+
const entries = fs$2.readdirSync(widgetPath, { withFileTypes: true });
|
|
29955
|
+
const subdirs = entries.filter(
|
|
29956
|
+
(e) =>
|
|
29957
|
+
e.isDirectory() &&
|
|
29958
|
+
!e.name.startsWith(".") &&
|
|
29959
|
+
e.name !== "dist" &&
|
|
29960
|
+
e.name !== "node_modules",
|
|
29961
|
+
);
|
|
29961
29962
|
|
|
29962
|
-
|
|
29963
|
-
|
|
29964
|
-
|
|
29965
|
-
|
|
29963
|
+
for (const subdir of subdirs) {
|
|
29964
|
+
const nested = path$5.join(widgetPath, subdir.name, "widgets");
|
|
29965
|
+
if (fs$2.existsSync(nested)) {
|
|
29966
|
+
console.log(`[WidgetCompiler] Found nested widgets/ at ${nested}`);
|
|
29967
|
+
return nested;
|
|
29968
|
+
}
|
|
29966
29969
|
}
|
|
29970
|
+
} catch (err) {
|
|
29971
|
+
// Non-fatal — fall through to null
|
|
29967
29972
|
}
|
|
29968
29973
|
|
|
29969
|
-
|
|
29970
|
-
* Clear cache
|
|
29971
|
-
*/
|
|
29972
|
-
clearCache() {
|
|
29973
|
-
this.loadedWidgets.clear();
|
|
29974
|
-
this.moduleCache.clear();
|
|
29975
|
-
}
|
|
29974
|
+
return null;
|
|
29976
29975
|
}
|
|
29977
29976
|
|
|
29978
|
-
const dynamicWidgetLoader$1 = new DynamicWidgetLoader();
|
|
29979
|
-
|
|
29980
|
-
dynamicWidgetLoader$2.exports = DynamicWidgetLoader;
|
|
29981
|
-
dynamicWidgetLoader$2.exports.dynamicWidgetLoader = dynamicWidgetLoader$1;
|
|
29982
|
-
|
|
29983
|
-
var dynamicWidgetLoaderExports = dynamicWidgetLoader$2.exports;
|
|
29984
|
-
|
|
29985
29977
|
/**
|
|
29986
|
-
*
|
|
29978
|
+
* Compile widget source files into a CJS bundle at dist/index.cjs.js.
|
|
29987
29979
|
*
|
|
29988
|
-
*
|
|
29989
|
-
*
|
|
29990
|
-
*
|
|
29980
|
+
* For each {Name}.dash.js found in the widgets/ directory, a synthetic
|
|
29981
|
+
* entry point is generated that imports the component + config and
|
|
29982
|
+
* re-exports them as `{ ...config, component }` — matching what
|
|
29983
|
+
* extractWidgetConfigs() in widgetBundleLoader.js expects.
|
|
29984
|
+
*
|
|
29985
|
+
* @param {string} widgetPath - Absolute path to the widget directory
|
|
29986
|
+
* @returns {Promise<string|null>} Path to the compiled bundle, or null if nothing to compile
|
|
29991
29987
|
*/
|
|
29988
|
+
async function compileWidget(widgetPath) {
|
|
29989
|
+
const widgetsDir = findWidgetsDir$1(widgetPath);
|
|
29992
29990
|
|
|
29993
|
-
|
|
29994
|
-
|
|
29995
|
-
|
|
29996
|
-
|
|
29997
|
-
|
|
29998
|
-
|
|
29999
|
-
const tasks = new Map(); // taskId -> task object
|
|
30000
|
-
const pendingResults = new Map(); // widgetId -> Array<{ taskId, taskKey, firedAt }>
|
|
29991
|
+
if (!widgetsDir) {
|
|
29992
|
+
console.log(
|
|
29993
|
+
`[WidgetCompiler] No widgets/ directory in ${widgetPath}, skipping`,
|
|
29994
|
+
);
|
|
29995
|
+
return null;
|
|
29996
|
+
}
|
|
30001
29997
|
|
|
30002
|
-
|
|
30003
|
-
const
|
|
30004
|
-
const
|
|
29998
|
+
// Discover .dash.js config files
|
|
29999
|
+
const files = fs$2.readdirSync(widgetsDir);
|
|
30000
|
+
const dashFiles = files.filter((f) => f.endsWith(".dash.js"));
|
|
30005
30001
|
|
|
30006
|
-
|
|
30007
|
-
|
|
30008
|
-
|
|
30009
|
-
|
|
30010
|
-
|
|
30011
|
-
|
|
30012
|
-
};
|
|
30002
|
+
if (dashFiles.length === 0) {
|
|
30003
|
+
console.log(
|
|
30004
|
+
`[WidgetCompiler] No .dash.js files found in ${widgetsDir}, skipping`,
|
|
30005
|
+
);
|
|
30006
|
+
return null;
|
|
30007
|
+
}
|
|
30013
30008
|
|
|
30014
|
-
//
|
|
30015
|
-
|
|
30016
|
-
|
|
30017
|
-
|
|
30018
|
-
|
|
30019
|
-
|
|
30020
|
-
|
|
30021
|
-
fri: 5,
|
|
30022
|
-
sat: 6,
|
|
30023
|
-
};
|
|
30009
|
+
// Build a synthetic entry point that pairs each component with its config.
|
|
30010
|
+
// Compute relative path from the entry file (in widgetPath) to widgetsDir,
|
|
30011
|
+
// since widgetsDir may be nested (e.g., ./weather-widget/widgets/).
|
|
30012
|
+
const relWidgetsDir =
|
|
30013
|
+
"./" + path$5.relative(widgetPath, widgetsDir).split(path$5.sep).join("/");
|
|
30014
|
+
const imports = [];
|
|
30015
|
+
const exportParts = [];
|
|
30024
30016
|
|
|
30025
|
-
|
|
30026
|
-
|
|
30027
|
-
|
|
30028
|
-
|
|
30029
|
-
|
|
30030
|
-
*/
|
|
30031
|
-
function buildCronExpression(days, time) {
|
|
30032
|
-
const [hours, minutes] = time.split(":").map(Number);
|
|
30033
|
-
if (days.includes("every")) {
|
|
30034
|
-
return `${minutes} ${hours} * * *`;
|
|
30035
|
-
}
|
|
30036
|
-
const dayNums = days.map((d) => DAY_MAP[d]).filter((n) => n !== undefined);
|
|
30037
|
-
return `${minutes} ${hours} * * ${dayNums.join(",")}`;
|
|
30038
|
-
}
|
|
30017
|
+
for (const dashFile of dashFiles) {
|
|
30018
|
+
const componentName = dashFile.replace(".dash.js", "");
|
|
30019
|
+
const componentFile = `${componentName}.js`;
|
|
30020
|
+
const componentFilePath = path$5.join(widgetsDir, componentFile);
|
|
30021
|
+
const hasComponent = fs$2.existsSync(componentFilePath);
|
|
30039
30022
|
|
|
30040
|
-
|
|
30041
|
-
|
|
30042
|
-
|
|
30043
|
-
|
|
30044
|
-
* @returns {number} next fire timestamp in ms
|
|
30045
|
-
*/
|
|
30046
|
-
function computeNextFire(task, now) {
|
|
30047
|
-
if (task.scheduleType === "interval") {
|
|
30048
|
-
return now + (task.intervalMs || 60000);
|
|
30049
|
-
}
|
|
30023
|
+
// Import the config (always)
|
|
30024
|
+
imports.push(
|
|
30025
|
+
`import ${componentName}Config from "${relWidgetsDir}/${dashFile}";`,
|
|
30026
|
+
);
|
|
30050
30027
|
|
|
30051
|
-
|
|
30052
|
-
|
|
30053
|
-
|
|
30054
|
-
|
|
30055
|
-
|
|
30056
|
-
|
|
30057
|
-
|
|
30058
|
-
|
|
30059
|
-
}
|
|
30060
|
-
|
|
30061
|
-
|
|
30062
|
-
|
|
30028
|
+
if (hasComponent) {
|
|
30029
|
+
// Import the component and merge with config
|
|
30030
|
+
imports.push(
|
|
30031
|
+
`import ${componentName}Comp from "${relWidgetsDir}/${componentFile}";`,
|
|
30032
|
+
);
|
|
30033
|
+
exportParts.push(
|
|
30034
|
+
`export const ${componentName} = { ...${componentName}Config, component: ${componentName}Comp };`,
|
|
30035
|
+
);
|
|
30036
|
+
} else {
|
|
30037
|
+
// Config-only (no component source file)
|
|
30038
|
+
exportParts.push(
|
|
30039
|
+
`export const ${componentName} = ${componentName}Config;`,
|
|
30063
30040
|
);
|
|
30064
30041
|
}
|
|
30065
|
-
// Fallback: 1 hour from now
|
|
30066
|
-
return now + 3600000;
|
|
30067
30042
|
}
|
|
30068
30043
|
|
|
30069
|
-
|
|
30070
|
-
return now + 3600000;
|
|
30071
|
-
}
|
|
30044
|
+
const entryContent = [...imports, "", ...exportParts, ""].join("\n");
|
|
30072
30045
|
|
|
30073
|
-
|
|
30074
|
-
|
|
30075
|
-
|
|
30076
|
-
|
|
30077
|
-
const payload = {
|
|
30078
|
-
taskId: task.taskId,
|
|
30079
|
-
widgetId: task.widgetId,
|
|
30080
|
-
taskKey: task.taskKey,
|
|
30081
|
-
handler: task.handler,
|
|
30082
|
-
firedAt: Date.now(),
|
|
30083
|
-
};
|
|
30084
|
-
|
|
30085
|
-
console.log(
|
|
30086
|
-
`[schedulerController] Fired: ${task.widgetName}.${task.taskKey} (${task.displayName})`,
|
|
30087
|
-
);
|
|
30088
|
-
|
|
30089
|
-
// Add to pending results queue
|
|
30090
|
-
let queue = pendingResults.get(task.widgetId) || [];
|
|
30091
|
-
queue.push({
|
|
30092
|
-
taskId: task.taskId,
|
|
30093
|
-
taskKey: task.taskKey,
|
|
30094
|
-
firedAt: payload.firedAt,
|
|
30095
|
-
});
|
|
30096
|
-
if (queue.length > MAX_PENDING_PER_WIDGET) {
|
|
30097
|
-
queue = queue.slice(-MAX_PENDING_PER_WIDGET);
|
|
30098
|
-
}
|
|
30099
|
-
pendingResults.set(task.widgetId, queue);
|
|
30046
|
+
// Write temporary entry file in the widget root
|
|
30047
|
+
const entryPath = path$5.join(widgetPath, "__compile_entry.js");
|
|
30048
|
+
const distDir = path$5.join(widgetPath, "dist");
|
|
30049
|
+
const outPath = path$5.join(distDir, "index.cjs.js");
|
|
30100
30050
|
|
|
30101
|
-
|
|
30102
|
-
|
|
30103
|
-
|
|
30104
|
-
|
|
30105
|
-
if (!win.isDestroyed()) {
|
|
30106
|
-
win.webContents.send("scheduler:task-fired", payload);
|
|
30107
|
-
}
|
|
30108
|
-
}
|
|
30109
|
-
} else {
|
|
30110
|
-
// No windows open — send native OS notification
|
|
30111
|
-
if (deps.notificationController && deps.getMainWindow) {
|
|
30112
|
-
deps.notificationController.send(deps.getMainWindow(), {
|
|
30113
|
-
widgetName: task.widgetName,
|
|
30114
|
-
widgetId: task.widgetId,
|
|
30115
|
-
workspaceId: task.workspaceId || "",
|
|
30116
|
-
type: "scheduled-task",
|
|
30117
|
-
title: task.displayName || task.taskKey,
|
|
30118
|
-
body: `Scheduled task "${task.displayName}" fired`,
|
|
30119
|
-
silent: false,
|
|
30120
|
-
});
|
|
30051
|
+
try {
|
|
30052
|
+
// Ensure dist/ directory exists
|
|
30053
|
+
if (!fs$2.existsSync(distDir)) {
|
|
30054
|
+
fs$2.mkdirSync(distDir, { recursive: true });
|
|
30121
30055
|
}
|
|
30122
|
-
}
|
|
30123
|
-
}
|
|
30124
30056
|
|
|
30125
|
-
|
|
30126
|
-
* Main tick — runs every 1s, checks all enabled tasks.
|
|
30127
|
-
*/
|
|
30128
|
-
function tick() {
|
|
30129
|
-
const now = Date.now();
|
|
30130
|
-
for (const [, task] of tasks) {
|
|
30131
|
-
if (!task.enabled || !task.nextFireAt || task.nextFireAt > now) continue;
|
|
30132
|
-
fireTask(task);
|
|
30133
|
-
task.nextFireAt = computeNextFire(task, now);
|
|
30134
|
-
task.lastFiredAt = now;
|
|
30135
|
-
task.fireCount = (task.fireCount || 0) + 1;
|
|
30136
|
-
}
|
|
30137
|
-
debouncedPersist();
|
|
30138
|
-
}
|
|
30057
|
+
fs$2.writeFileSync(entryPath, entryContent, "utf8");
|
|
30139
30058
|
|
|
30140
|
-
|
|
30141
|
-
|
|
30142
|
-
|
|
30143
|
-
function debouncedPersist() {
|
|
30144
|
-
if (persistTimeout) return;
|
|
30145
|
-
persistTimeout = setTimeout(() => {
|
|
30146
|
-
persistTimeout = null;
|
|
30147
|
-
persistNow();
|
|
30148
|
-
}, PERSIST_DEBOUNCE_MS);
|
|
30149
|
-
}
|
|
30059
|
+
console.log(
|
|
30060
|
+
`[WidgetCompiler] Compiling ${dashFiles.length} component(s) from ${widgetPath}`,
|
|
30061
|
+
);
|
|
30150
30062
|
|
|
30151
|
-
|
|
30152
|
-
|
|
30153
|
-
const
|
|
30154
|
-
for (const [taskId, task] of tasks) {
|
|
30155
|
-
data[taskId] = { ...task };
|
|
30156
|
-
}
|
|
30157
|
-
store$3.set("tasks", data);
|
|
30158
|
-
} catch (err) {
|
|
30159
|
-
console.error("[schedulerController] Error persisting tasks:", err);
|
|
30160
|
-
}
|
|
30161
|
-
}
|
|
30063
|
+
// Lazy-require esbuild so the module doesn't fail to load if
|
|
30064
|
+
// esbuild is not yet installed (e.g., during first npm install)
|
|
30065
|
+
const esbuild = require("esbuild");
|
|
30162
30066
|
|
|
30163
|
-
|
|
30164
|
-
|
|
30165
|
-
|
|
30166
|
-
|
|
30167
|
-
|
|
30168
|
-
|
|
30169
|
-
|
|
30170
|
-
|
|
30171
|
-
|
|
30172
|
-
|
|
30173
|
-
|
|
30067
|
+
await esbuild.build({
|
|
30068
|
+
entryPoints: [entryPath],
|
|
30069
|
+
bundle: true,
|
|
30070
|
+
format: "cjs",
|
|
30071
|
+
outfile: outPath,
|
|
30072
|
+
// These modules are provided by the host app via MODULE_MAP
|
|
30073
|
+
// in widgetBundleLoader.js — do NOT bundle them
|
|
30074
|
+
external: [
|
|
30075
|
+
"react",
|
|
30076
|
+
"react-dom",
|
|
30077
|
+
"@trops/dash-react",
|
|
30078
|
+
"@trops/dash-core",
|
|
30079
|
+
"react/jsx-runtime",
|
|
30080
|
+
"prop-types",
|
|
30081
|
+
],
|
|
30082
|
+
// Treat .js files as JSX (widget sources use JSX in .js files)
|
|
30083
|
+
loader: { ".js": "jsx" },
|
|
30084
|
+
logLevel: "warning",
|
|
30085
|
+
});
|
|
30086
|
+
|
|
30087
|
+
console.log(`[WidgetCompiler] Compiled successfully → ${outPath}`);
|
|
30088
|
+
return outPath;
|
|
30089
|
+
} catch (error) {
|
|
30090
|
+
console.error(
|
|
30091
|
+
`[WidgetCompiler] Compilation failed for ${widgetPath}:`,
|
|
30092
|
+
error,
|
|
30093
|
+
);
|
|
30094
|
+
throw error;
|
|
30095
|
+
} finally {
|
|
30096
|
+
// Clean up temporary entry file
|
|
30097
|
+
try {
|
|
30098
|
+
if (fs$2.existsSync(entryPath)) {
|
|
30099
|
+
fs$2.unlinkSync(entryPath);
|
|
30174
30100
|
}
|
|
30175
|
-
|
|
30101
|
+
} catch (cleanupError) {
|
|
30102
|
+
// Non-fatal
|
|
30103
|
+
console.warn(
|
|
30104
|
+
`[WidgetCompiler] Could not remove temp entry file:`,
|
|
30105
|
+
cleanupError,
|
|
30106
|
+
);
|
|
30176
30107
|
}
|
|
30177
|
-
console.log(`[schedulerController] Loaded ${tasks.size} tasks from store`);
|
|
30178
|
-
} catch (err) {
|
|
30179
|
-
console.error("[schedulerController] Error loading tasks:", err);
|
|
30180
30108
|
}
|
|
30181
30109
|
}
|
|
30182
30110
|
|
|
30111
|
+
var widgetCompiler$1 = { compileWidget, findWidgetsDir: findWidgetsDir$1 };
|
|
30112
|
+
|
|
30183
30113
|
/**
|
|
30184
|
-
*
|
|
30114
|
+
* Dynamic Widget Loader
|
|
30115
|
+
*
|
|
30116
|
+
* Loads React components and configurations from downloaded/local widget paths
|
|
30117
|
+
* Works with widgets that follow the Dash widget structure:
|
|
30118
|
+
* - widgets/
|
|
30119
|
+
* - WidgetName.js (React component)
|
|
30120
|
+
* - WidgetName.dash.js (configuration)
|
|
30121
|
+
*
|
|
30122
|
+
* Integrates with ComponentManager for automatic registration
|
|
30185
30123
|
*/
|
|
30186
|
-
function countTasksForWidget(widgetId) {
|
|
30187
|
-
let count = 0;
|
|
30188
|
-
for (const [, task] of tasks) {
|
|
30189
|
-
if (task.widgetId === widgetId) count++;
|
|
30190
|
-
}
|
|
30191
|
-
return count;
|
|
30192
|
-
}
|
|
30193
30124
|
|
|
30194
|
-
const
|
|
30195
|
-
|
|
30196
|
-
|
|
30197
|
-
|
|
30198
|
-
init({ getWindows, notificationController, getMainWindow }) {
|
|
30199
|
-
deps.getWindows = getWindows;
|
|
30200
|
-
deps.notificationController = notificationController;
|
|
30201
|
-
deps.getMainWindow = getMainWindow;
|
|
30202
|
-
},
|
|
30125
|
+
const fs$1 = require$$0$3;
|
|
30126
|
+
const path$4 = require$$1$2;
|
|
30127
|
+
const vm = require$$2$3;
|
|
30128
|
+
const { findWidgetsDir } = widgetCompiler$1;
|
|
30203
30129
|
|
|
30204
|
-
|
|
30205
|
-
|
|
30206
|
-
|
|
30207
|
-
|
|
30208
|
-
|
|
30209
|
-
|
|
30210
|
-
tickInterval = setInterval(tick, 1000);
|
|
30211
|
-
console.log("[schedulerController] Tick loop started");
|
|
30212
|
-
}
|
|
30213
|
-
},
|
|
30130
|
+
class DynamicWidgetLoader {
|
|
30131
|
+
constructor(componentManager = null) {
|
|
30132
|
+
this.loadedWidgets = new Map();
|
|
30133
|
+
this.moduleCache = new Map();
|
|
30134
|
+
this.componentManager = componentManager;
|
|
30135
|
+
}
|
|
30214
30136
|
|
|
30215
30137
|
/**
|
|
30216
|
-
*
|
|
30138
|
+
* Set ComponentManager instance for automatic widget registration
|
|
30139
|
+
* @param {Object} manager - ComponentManager instance from @trops/dash-react
|
|
30217
30140
|
*/
|
|
30218
|
-
|
|
30219
|
-
|
|
30220
|
-
|
|
30221
|
-
tickInterval = null;
|
|
30222
|
-
}
|
|
30223
|
-
if (persistTimeout) {
|
|
30224
|
-
clearTimeout(persistTimeout);
|
|
30225
|
-
persistTimeout = null;
|
|
30226
|
-
}
|
|
30227
|
-
persistNow();
|
|
30228
|
-
console.log("[schedulerController] Stopped");
|
|
30229
|
-
},
|
|
30141
|
+
setComponentManager(manager) {
|
|
30142
|
+
this.componentManager = manager;
|
|
30143
|
+
}
|
|
30230
30144
|
|
|
30231
30145
|
/**
|
|
30232
|
-
*
|
|
30233
|
-
*
|
|
30234
|
-
* @param {
|
|
30235
|
-
* @param {string}
|
|
30236
|
-
* @param {
|
|
30237
|
-
* @
|
|
30238
|
-
* @param {string} payload.taskKey - key from .dash.js
|
|
30239
|
-
* @param {string} payload.handler - handler function name
|
|
30240
|
-
* @param {string} payload.displayName - human-readable name
|
|
30241
|
-
* @param {string} payload.scheduleType - "interval" | "dayTime"
|
|
30242
|
-
* @param {number} [payload.intervalMs] - for interval type
|
|
30243
|
-
* @param {string[]} [payload.days] - for dayTime type
|
|
30244
|
-
* @param {string} [payload.time] - for dayTime type (HH:mm)
|
|
30245
|
-
* @param {boolean} [payload.enabled] - defaults to true
|
|
30246
|
-
* @returns {{ success: boolean, taskId?: string, error?: string }}
|
|
30146
|
+
* Load a widget from a local path
|
|
30147
|
+
* @param {string} widgetName - Name of the widget (e.g., "MyFirstWidget")
|
|
30148
|
+
* @param {string} widgetPath - Path to the widget directory
|
|
30149
|
+
* @param {string} componentName - Name of the component file (e.g., "MyFirstWidgetWidget")
|
|
30150
|
+
* @param {boolean} autoRegister - Automatically register with ComponentManager (if available)
|
|
30151
|
+
* @returns {Promise<Object>} { component, config, registered }
|
|
30247
30152
|
*/
|
|
30248
|
-
|
|
30153
|
+
async loadWidget(widgetName, widgetPath, componentName, autoRegister = true) {
|
|
30249
30154
|
try {
|
|
30250
|
-
const {
|
|
30251
|
-
widgetId,
|
|
30252
|
-
widgetName,
|
|
30253
|
-
workspaceId,
|
|
30254
|
-
taskKey,
|
|
30255
|
-
handler,
|
|
30256
|
-
displayName,
|
|
30257
|
-
scheduleType,
|
|
30258
|
-
intervalMs,
|
|
30259
|
-
days,
|
|
30260
|
-
time,
|
|
30261
|
-
} = payload;
|
|
30155
|
+
const cacheKey = `${widgetName}:${componentName}`;
|
|
30262
30156
|
|
|
30263
|
-
|
|
30264
|
-
|
|
30157
|
+
if (this.loadedWidgets.has(cacheKey)) {
|
|
30158
|
+
console.log(`[DynamicWidgetLoader] Loading ${widgetName} from cache`);
|
|
30159
|
+
return this.loadedWidgets.get(cacheKey);
|
|
30160
|
+
}
|
|
30265
30161
|
|
|
30266
|
-
|
|
30267
|
-
|
|
30268
|
-
|
|
30162
|
+
console.log(
|
|
30163
|
+
`[DynamicWidgetLoader] Loading widget: ${widgetName} from ${widgetPath}`,
|
|
30164
|
+
);
|
|
30165
|
+
|
|
30166
|
+
const widgetsDir =
|
|
30167
|
+
findWidgetsDir(widgetPath) || path$4.join(widgetPath, "widgets");
|
|
30168
|
+
const componentPath = path$4.join(widgetsDir, `${componentName}.js`);
|
|
30169
|
+
const configPath = path$4.join(widgetsDir, `${componentName}.dash.js`);
|
|
30170
|
+
|
|
30171
|
+
if (!fs$1.existsSync(componentPath)) {
|
|
30172
|
+
throw new Error(`Component file not found: ${componentPath}`);
|
|
30173
|
+
}
|
|
30174
|
+
if (!fs$1.existsSync(configPath)) {
|
|
30175
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
30269
30176
|
}
|
|
30270
30177
|
|
|
30271
|
-
const
|
|
30272
|
-
|
|
30273
|
-
|
|
30274
|
-
|
|
30275
|
-
|
|
30276
|
-
workspaceId: workspaceId || existing?.workspaceId || "",
|
|
30277
|
-
taskKey,
|
|
30278
|
-
handler: handler || existing?.handler || taskKey,
|
|
30279
|
-
displayName: displayName || existing?.displayName || taskKey,
|
|
30280
|
-
scheduleType: scheduleType || existing?.scheduleType || "interval",
|
|
30281
|
-
intervalMs:
|
|
30282
|
-
intervalMs !== undefined ? intervalMs : existing?.intervalMs || null,
|
|
30283
|
-
days: days !== undefined ? days : existing?.days || null,
|
|
30284
|
-
time: time !== undefined ? time : existing?.time || null,
|
|
30285
|
-
enabled:
|
|
30286
|
-
payload.enabled !== undefined
|
|
30287
|
-
? payload.enabled
|
|
30288
|
-
: existing?.enabled !== undefined
|
|
30289
|
-
? existing.enabled
|
|
30290
|
-
: true,
|
|
30291
|
-
nextFireAt: 0,
|
|
30292
|
-
lastFiredAt: existing?.lastFiredAt || null,
|
|
30293
|
-
fireCount: existing?.fireCount || 0,
|
|
30294
|
-
createdAt: existing?.createdAt || new Date().toISOString(),
|
|
30178
|
+
const config = await this.loadConfigFile(configPath);
|
|
30179
|
+
|
|
30180
|
+
const component = {
|
|
30181
|
+
path: componentPath,
|
|
30182
|
+
name: componentName,
|
|
30295
30183
|
};
|
|
30296
30184
|
|
|
30297
|
-
|
|
30298
|
-
task.nextFireAt = task.enabled ? computeNextFire(task, now) : 0;
|
|
30185
|
+
let registered = false;
|
|
30299
30186
|
|
|
30300
|
-
|
|
30301
|
-
|
|
30187
|
+
if (autoRegister && this.componentManager) {
|
|
30188
|
+
try {
|
|
30189
|
+
// Use scoped id as registration key if available,
|
|
30190
|
+
// otherwise fall back to componentName
|
|
30191
|
+
const registrationKey = config.id || componentName;
|
|
30192
|
+
this.componentManager.registerWidget(config, registrationKey);
|
|
30193
|
+
registered = true;
|
|
30194
|
+
console.log(
|
|
30195
|
+
`[DynamicWidgetLoader] ✓ Registered ${registrationKey} with ComponentManager`,
|
|
30196
|
+
);
|
|
30197
|
+
} catch (regError) {
|
|
30198
|
+
console.warn(
|
|
30199
|
+
`[DynamicWidgetLoader] Failed to register with ComponentManager:`,
|
|
30200
|
+
regError,
|
|
30201
|
+
);
|
|
30202
|
+
}
|
|
30203
|
+
}
|
|
30302
30204
|
|
|
30303
|
-
|
|
30304
|
-
|
|
30305
|
-
);
|
|
30205
|
+
const result = { component, config, registered };
|
|
30206
|
+
this.loadedWidgets.set(cacheKey, result);
|
|
30306
30207
|
|
|
30307
|
-
return
|
|
30208
|
+
return result;
|
|
30308
30209
|
} catch (error) {
|
|
30309
|
-
console.error(
|
|
30310
|
-
|
|
30210
|
+
console.error(
|
|
30211
|
+
`[DynamicWidgetLoader] Error loading widget ${widgetName}:`,
|
|
30212
|
+
error,
|
|
30213
|
+
);
|
|
30214
|
+
throw error;
|
|
30311
30215
|
}
|
|
30312
|
-
}
|
|
30216
|
+
}
|
|
30313
30217
|
|
|
30314
30218
|
/**
|
|
30315
|
-
*
|
|
30316
|
-
* @param {string}
|
|
30317
|
-
* @returns {
|
|
30219
|
+
* Load and parse a .dash.js configuration file
|
|
30220
|
+
* @param {string} configPath - Path to the .dash.js file
|
|
30221
|
+
* @returns {Promise<Object>} Configuration object
|
|
30318
30222
|
*/
|
|
30319
|
-
|
|
30320
|
-
|
|
30321
|
-
|
|
30322
|
-
debouncedPersist();
|
|
30323
|
-
console.log(`[schedulerController] Removed: ${taskId}`);
|
|
30324
|
-
}
|
|
30325
|
-
return { success: deleted };
|
|
30326
|
-
},
|
|
30223
|
+
async loadConfigFile(configPath) {
|
|
30224
|
+
try {
|
|
30225
|
+
const source = fs$1.readFileSync(configPath, "utf8");
|
|
30327
30226
|
|
|
30328
|
-
|
|
30329
|
-
|
|
30330
|
-
|
|
30331
|
-
|
|
30332
|
-
|
|
30333
|
-
|
|
30334
|
-
|
|
30335
|
-
|
|
30336
|
-
|
|
30337
|
-
|
|
30338
|
-
|
|
30227
|
+
let exportMatch = source.match(/export\s+default\s+({[\s\S]*});?\s*$/);
|
|
30228
|
+
|
|
30229
|
+
// Handle variable export pattern: const x = {...}; export default x;
|
|
30230
|
+
if (!exportMatch) {
|
|
30231
|
+
const varExportMatch = source.match(
|
|
30232
|
+
/export\s+default\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*;?\s*$/,
|
|
30233
|
+
);
|
|
30234
|
+
if (varExportMatch) {
|
|
30235
|
+
const varName = varExportMatch[1];
|
|
30236
|
+
const varDeclMatch = source.match(
|
|
30237
|
+
new RegExp(
|
|
30238
|
+
`(?:const|let|var)\\s+${varName}\\s*=\\s*({[\\s\\S]*?});\\s*(?:export\\s+default)`,
|
|
30239
|
+
),
|
|
30240
|
+
);
|
|
30241
|
+
if (varDeclMatch) {
|
|
30242
|
+
exportMatch = varDeclMatch;
|
|
30243
|
+
}
|
|
30244
|
+
}
|
|
30339
30245
|
}
|
|
30340
|
-
}
|
|
30341
|
-
if (count > 0) {
|
|
30342
|
-
debouncedPersist();
|
|
30343
|
-
pendingResults.delete(widgetId);
|
|
30344
|
-
}
|
|
30345
|
-
console.log(
|
|
30346
|
-
`[schedulerController] Removed ${count} tasks for widget ${widgetId}`,
|
|
30347
|
-
);
|
|
30348
|
-
return { success: true, count };
|
|
30349
|
-
},
|
|
30350
30246
|
|
|
30351
|
-
|
|
30352
|
-
|
|
30353
|
-
* @param {string} widgetId
|
|
30354
|
-
* @returns {Object[]}
|
|
30355
|
-
*/
|
|
30356
|
-
getTasks(widgetId) {
|
|
30357
|
-
const result = [];
|
|
30358
|
-
for (const [, task] of tasks) {
|
|
30359
|
-
if (task.widgetId === widgetId) {
|
|
30360
|
-
result.push({ ...task });
|
|
30247
|
+
if (!exportMatch) {
|
|
30248
|
+
throw new Error("Could not find default export in config file");
|
|
30361
30249
|
}
|
|
30250
|
+
|
|
30251
|
+
// Sanitize component references so vm.runInContext doesn't fail
|
|
30252
|
+
// on unresolvable imports — replace component: SomeName with component: "SomeName"
|
|
30253
|
+
const exportedObjectStr = exportMatch[1].replace(
|
|
30254
|
+
/component\s*:\s*([A-Z][a-zA-Z0-9_$]*)/g,
|
|
30255
|
+
'component: "$1"',
|
|
30256
|
+
);
|
|
30257
|
+
|
|
30258
|
+
const context = vm.createContext({ module: { exports: {} } });
|
|
30259
|
+
vm.runInContext(`module.exports = ${exportedObjectStr}`, context);
|
|
30260
|
+
|
|
30261
|
+
return context.module.exports;
|
|
30262
|
+
} catch (error) {
|
|
30263
|
+
console.error(`[DynamicWidgetLoader] Error loading config:`, error);
|
|
30264
|
+
throw error;
|
|
30362
30265
|
}
|
|
30363
|
-
|
|
30364
|
-
},
|
|
30266
|
+
}
|
|
30365
30267
|
|
|
30366
30268
|
/**
|
|
30367
|
-
*
|
|
30368
|
-
* @param {string}
|
|
30369
|
-
* @
|
|
30370
|
-
* @returns {{ success: boolean }}
|
|
30269
|
+
* Discover available widgets in a directory
|
|
30270
|
+
* @param {string} widgetPath - Path to search for widgets
|
|
30271
|
+
* @returns {Array} List of available widget names
|
|
30371
30272
|
*/
|
|
30372
|
-
|
|
30373
|
-
|
|
30374
|
-
|
|
30375
|
-
|
|
30376
|
-
|
|
30377
|
-
|
|
30378
|
-
const allowedFields = [
|
|
30379
|
-
"scheduleType",
|
|
30380
|
-
"intervalMs",
|
|
30381
|
-
"days",
|
|
30382
|
-
"time",
|
|
30383
|
-
"displayName",
|
|
30384
|
-
"enabled",
|
|
30385
|
-
];
|
|
30386
|
-
for (const key of allowedFields) {
|
|
30387
|
-
if (updates[key] !== undefined) {
|
|
30388
|
-
task[key] = updates[key];
|
|
30273
|
+
discoverWidgets(widgetPath) {
|
|
30274
|
+
try {
|
|
30275
|
+
const widgetsDir = findWidgetsDir(widgetPath);
|
|
30276
|
+
if (!widgetsDir) {
|
|
30277
|
+
return [];
|
|
30389
30278
|
}
|
|
30390
|
-
}
|
|
30391
30279
|
|
|
30392
|
-
|
|
30393
|
-
|
|
30394
|
-
task.nextFireAt = computeNextFire(task, Date.now());
|
|
30395
|
-
} else {
|
|
30396
|
-
task.nextFireAt = 0;
|
|
30397
|
-
}
|
|
30280
|
+
const files = fs$1.readdirSync(widgetsDir);
|
|
30281
|
+
const widgets = new Set();
|
|
30398
30282
|
|
|
30399
|
-
|
|
30400
|
-
|
|
30401
|
-
|
|
30402
|
-
|
|
30283
|
+
files.forEach((file) => {
|
|
30284
|
+
if (file.endsWith(".dash.js")) {
|
|
30285
|
+
const componentName = file.replace(".dash.js", "");
|
|
30286
|
+
widgets.add(componentName);
|
|
30287
|
+
}
|
|
30288
|
+
});
|
|
30403
30289
|
|
|
30404
|
-
|
|
30405
|
-
|
|
30406
|
-
|
|
30407
|
-
|
|
30408
|
-
|
|
30409
|
-
|
|
30410
|
-
const task = tasks.get(taskId);
|
|
30411
|
-
if (!task) return { success: false, error: "task_not_found" };
|
|
30412
|
-
task.enabled = true;
|
|
30413
|
-
task.nextFireAt = computeNextFire(task, Date.now());
|
|
30414
|
-
debouncedPersist();
|
|
30415
|
-
console.log(`[schedulerController] Enabled: ${taskId}`);
|
|
30416
|
-
return { success: true };
|
|
30417
|
-
},
|
|
30290
|
+
return Array.from(widgets);
|
|
30291
|
+
} catch (error) {
|
|
30292
|
+
console.error(`[DynamicWidgetLoader] Error discovering widgets:`, error);
|
|
30293
|
+
return [];
|
|
30294
|
+
}
|
|
30295
|
+
}
|
|
30418
30296
|
|
|
30419
30297
|
/**
|
|
30420
|
-
*
|
|
30421
|
-
* @param {string} taskId
|
|
30422
|
-
* @returns {{ success: boolean }}
|
|
30298
|
+
* Clear cache
|
|
30423
30299
|
*/
|
|
30424
|
-
|
|
30425
|
-
|
|
30426
|
-
|
|
30427
|
-
|
|
30428
|
-
|
|
30429
|
-
debouncedPersist();
|
|
30430
|
-
console.log(`[schedulerController] Disabled: ${taskId}`);
|
|
30431
|
-
return { success: true };
|
|
30432
|
-
},
|
|
30300
|
+
clearCache() {
|
|
30301
|
+
this.loadedWidgets.clear();
|
|
30302
|
+
this.moduleCache.clear();
|
|
30303
|
+
}
|
|
30304
|
+
}
|
|
30433
30305
|
|
|
30434
|
-
|
|
30435
|
-
* Drain pending fire results for a widget.
|
|
30436
|
-
* @param {string} widgetId
|
|
30437
|
-
* @returns {Object[]}
|
|
30438
|
-
*/
|
|
30439
|
-
getPendingResults(widgetId) {
|
|
30440
|
-
const queue = pendingResults.get(widgetId) || [];
|
|
30441
|
-
pendingResults.delete(widgetId);
|
|
30442
|
-
return queue;
|
|
30443
|
-
},
|
|
30444
|
-
|
|
30445
|
-
/**
|
|
30446
|
-
* Handle system suspend — stop tick loop.
|
|
30447
|
-
*/
|
|
30448
|
-
handleSuspend() {
|
|
30449
|
-
if (tickInterval) {
|
|
30450
|
-
clearInterval(tickInterval);
|
|
30451
|
-
tickInterval = null;
|
|
30452
|
-
console.log("[schedulerController] Suspended (tick stopped)");
|
|
30453
|
-
}
|
|
30454
|
-
},
|
|
30455
|
-
|
|
30456
|
-
/**
|
|
30457
|
-
* Handle system resume — fire missed tasks, restart tick.
|
|
30458
|
-
*/
|
|
30459
|
-
handleResume() {
|
|
30460
|
-
const now = Date.now();
|
|
30461
|
-
for (const [, task] of tasks) {
|
|
30462
|
-
if (task.enabled && task.nextFireAt && task.nextFireAt <= now) {
|
|
30463
|
-
fireTask(task);
|
|
30464
|
-
task.nextFireAt = computeNextFire(task, now);
|
|
30465
|
-
task.lastFiredAt = now;
|
|
30466
|
-
task.fireCount = (task.fireCount || 0) + 1;
|
|
30467
|
-
}
|
|
30468
|
-
}
|
|
30469
|
-
debouncedPersist();
|
|
30470
|
-
if (!tickInterval) {
|
|
30471
|
-
tickInterval = setInterval(tick, 1000);
|
|
30472
|
-
console.log("[schedulerController] Resumed (tick restarted)");
|
|
30473
|
-
}
|
|
30474
|
-
},
|
|
30306
|
+
const dynamicWidgetLoader$1 = new DynamicWidgetLoader();
|
|
30475
30307
|
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
* @param {string} widgetName - component name
|
|
30479
|
-
*/
|
|
30480
|
-
cleanupWidget(widgetName) {
|
|
30481
|
-
let count = 0;
|
|
30482
|
-
for (const [taskId, task] of tasks) {
|
|
30483
|
-
if (task.widgetName === widgetName) {
|
|
30484
|
-
tasks.delete(taskId);
|
|
30485
|
-
pendingResults.delete(task.widgetId);
|
|
30486
|
-
count++;
|
|
30487
|
-
}
|
|
30488
|
-
}
|
|
30489
|
-
if (count > 0) {
|
|
30490
|
-
debouncedPersist();
|
|
30491
|
-
console.log(
|
|
30492
|
-
`[schedulerController] Cleaned up ${count} tasks for widget "${widgetName}"`,
|
|
30493
|
-
);
|
|
30494
|
-
}
|
|
30495
|
-
},
|
|
30496
|
-
};
|
|
30308
|
+
dynamicWidgetLoader$2.exports = DynamicWidgetLoader;
|
|
30309
|
+
dynamicWidgetLoader$2.exports.dynamicWidgetLoader = dynamicWidgetLoader$1;
|
|
30497
30310
|
|
|
30498
|
-
var
|
|
30311
|
+
var dynamicWidgetLoaderExports = dynamicWidgetLoader$2.exports;
|
|
30499
30312
|
|
|
30500
30313
|
/**
|
|
30501
|
-
*
|
|
30502
|
-
*
|
|
30503
|
-
* Manages widget discovery, download, and dynamic loading
|
|
30504
|
-
* Widgets are expected to have:
|
|
30505
|
-
* - package.json (or dash.json) with metadata
|
|
30506
|
-
* - widgets/ folder containing [WidgetName].js and [WidgetName].dash.js
|
|
30314
|
+
* schedulerController.js
|
|
30507
30315
|
*
|
|
30508
|
-
*
|
|
30509
|
-
*
|
|
30510
|
-
* -
|
|
30511
|
-
* - Linux: ~/.config/[appName]/
|
|
30316
|
+
* Main process controller for widget scheduled tasks.
|
|
30317
|
+
* Manages a tick loop (1s resolution), persistence (electron-store),
|
|
30318
|
+
* and dispatching task-fired events to renderer windows.
|
|
30512
30319
|
*/
|
|
30513
30320
|
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
const path = require$$1$2;
|
|
30517
|
-
const os = require$$2$4;
|
|
30518
|
-
const AdmZip = require$$3$4;
|
|
30519
|
-
const { fileURLToPath } = require$$4$1;
|
|
30520
|
-
const { app, ipcMain, BrowserWindow } = require$$0$2;
|
|
30521
|
-
const { dynamicWidgetLoader } = dynamicWidgetLoaderExports;
|
|
30522
|
-
const { compileWidget, findWidgetsDir } = widgetCompiler$1;
|
|
30321
|
+
const Store$1 = require$$1$1;
|
|
30322
|
+
const { Cron } = require$$1$5;
|
|
30523
30323
|
|
|
30524
|
-
|
|
30525
|
-
let REGISTRY_CONFIG_FILE = null;
|
|
30324
|
+
const store$2 = new Store$1({ name: "dash-scheduler" });
|
|
30526
30325
|
|
|
30527
|
-
|
|
30528
|
-
|
|
30529
|
-
|
|
30530
|
-
* write outside the target extraction directory.
|
|
30531
|
-
* @param {AdmZip} zip - AdmZip instance
|
|
30532
|
-
* @param {string} targetDir - Intended extraction directory
|
|
30533
|
-
* @throws {Error} If any entry would escape the target directory
|
|
30534
|
-
*/
|
|
30535
|
-
function validateZipEntries(zip, targetDir) {
|
|
30536
|
-
const resolvedTarget = path.resolve(targetDir);
|
|
30537
|
-
for (const entry of zip.getEntries()) {
|
|
30538
|
-
const entryPath = entry.entryName;
|
|
30539
|
-
// Reject entries with '..' path segments
|
|
30540
|
-
if (
|
|
30541
|
-
entryPath.split("/").includes("..") ||
|
|
30542
|
-
entryPath.split("\\").includes("..")
|
|
30543
|
-
) {
|
|
30544
|
-
throw new Error(
|
|
30545
|
-
`Malicious ZIP entry rejected (path traversal): ${entryPath}`,
|
|
30546
|
-
);
|
|
30547
|
-
}
|
|
30548
|
-
// Reject absolute paths
|
|
30549
|
-
if (path.isAbsolute(entryPath)) {
|
|
30550
|
-
throw new Error(
|
|
30551
|
-
`Malicious ZIP entry rejected (absolute path): ${entryPath}`,
|
|
30552
|
-
);
|
|
30553
|
-
}
|
|
30554
|
-
// Final check: resolved path must be within target directory
|
|
30555
|
-
const resolvedEntry = path.resolve(resolvedTarget, entryPath);
|
|
30556
|
-
if (
|
|
30557
|
-
!resolvedEntry.startsWith(resolvedTarget + path.sep) &&
|
|
30558
|
-
resolvedEntry !== resolvedTarget
|
|
30559
|
-
) {
|
|
30560
|
-
throw new Error(
|
|
30561
|
-
`Malicious ZIP entry rejected (escapes target): ${entryPath}`,
|
|
30562
|
-
);
|
|
30563
|
-
}
|
|
30564
|
-
}
|
|
30565
|
-
}
|
|
30326
|
+
// --- In-memory state ---
|
|
30327
|
+
const tasks = new Map(); // taskId -> task object
|
|
30328
|
+
const pendingResults = new Map(); // widgetId -> Array<{ taskId, taskKey, firedAt }>
|
|
30566
30329
|
|
|
30567
|
-
|
|
30568
|
-
|
|
30569
|
-
|
|
30570
|
-
*/
|
|
30571
|
-
function initializeRegistry(customPath = null) {
|
|
30572
|
-
if (customPath) {
|
|
30573
|
-
WIDGETS_CACHE_DIR = path.join(customPath, "widgets");
|
|
30574
|
-
} else {
|
|
30575
|
-
WIDGETS_CACHE_DIR = path.join(app.getPath("userData"), "widgets");
|
|
30576
|
-
}
|
|
30577
|
-
REGISTRY_CONFIG_FILE = path.join(WIDGETS_CACHE_DIR, "registry.json");
|
|
30578
|
-
console.log(`[WidgetRegistry] Using storage path: ${WIDGETS_CACHE_DIR}`);
|
|
30579
|
-
}
|
|
30330
|
+
const MAX_TASKS_PER_WIDGET = 20;
|
|
30331
|
+
const MAX_PENDING_PER_WIDGET = 100;
|
|
30332
|
+
const PERSIST_DEBOUNCE_MS = 30_000;
|
|
30580
30333
|
|
|
30581
|
-
|
|
30582
|
-
|
|
30583
|
-
|
|
30584
|
-
|
|
30585
|
-
|
|
30334
|
+
let tickInterval = null;
|
|
30335
|
+
let persistTimeout = null;
|
|
30336
|
+
let deps = {
|
|
30337
|
+
getWindows: null,
|
|
30338
|
+
notificationController: null,
|
|
30339
|
+
getMainWindow: null,
|
|
30340
|
+
};
|
|
30586
30341
|
|
|
30587
|
-
|
|
30588
|
-
|
|
30589
|
-
|
|
30590
|
-
|
|
30591
|
-
|
|
30342
|
+
// --- Day name to cron day number ---
|
|
30343
|
+
const DAY_MAP = {
|
|
30344
|
+
sun: 0,
|
|
30345
|
+
mon: 1,
|
|
30346
|
+
tue: 2,
|
|
30347
|
+
wed: 3,
|
|
30348
|
+
thu: 4,
|
|
30349
|
+
fri: 5,
|
|
30350
|
+
sat: 6,
|
|
30351
|
+
};
|
|
30592
30352
|
|
|
30593
|
-
|
|
30594
|
-
|
|
30595
|
-
|
|
30596
|
-
|
|
30597
|
-
|
|
30598
|
-
|
|
30599
|
-
|
|
30600
|
-
|
|
30353
|
+
/**
|
|
30354
|
+
* Build a cron expression from days + time for use with croner.
|
|
30355
|
+
* @param {string[]} days - ["mon","wed","fri"] or ["every"]
|
|
30356
|
+
* @param {string} time - "09:00" (HH:mm)
|
|
30357
|
+
* @returns {string} cron expression
|
|
30358
|
+
*/
|
|
30359
|
+
function buildCronExpression(days, time) {
|
|
30360
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
30361
|
+
if (days.includes("every")) {
|
|
30362
|
+
return `${minutes} ${hours} * * *`;
|
|
30363
|
+
}
|
|
30364
|
+
const dayNums = days.map((d) => DAY_MAP[d]).filter((n) => n !== undefined);
|
|
30365
|
+
return `${minutes} ${hours} * * ${dayNums.join(",")}`;
|
|
30366
|
+
}
|
|
30601
30367
|
|
|
30602
|
-
|
|
30603
|
-
|
|
30604
|
-
|
|
30605
|
-
|
|
30606
|
-
|
|
30607
|
-
|
|
30608
|
-
|
|
30368
|
+
/**
|
|
30369
|
+
* Compute the next fire timestamp for a task.
|
|
30370
|
+
* @param {Object} task
|
|
30371
|
+
* @param {number} now - current timestamp in ms
|
|
30372
|
+
* @returns {number} next fire timestamp in ms
|
|
30373
|
+
*/
|
|
30374
|
+
function computeNextFire(task, now) {
|
|
30375
|
+
if (task.scheduleType === "interval") {
|
|
30376
|
+
return now + (task.intervalMs || 60000);
|
|
30377
|
+
}
|
|
30609
30378
|
|
|
30610
|
-
|
|
30611
|
-
|
|
30612
|
-
|
|
30613
|
-
|
|
30614
|
-
|
|
30615
|
-
|
|
30616
|
-
|
|
30617
|
-
|
|
30379
|
+
if (task.scheduleType === "dayTime" && task.days && task.time) {
|
|
30380
|
+
try {
|
|
30381
|
+
const cronExpr = buildCronExpression(task.days, task.time);
|
|
30382
|
+
const job = new Cron(cronExpr);
|
|
30383
|
+
const next = job.nextRun();
|
|
30384
|
+
if (next) {
|
|
30385
|
+
return next.getTime();
|
|
30386
|
+
}
|
|
30387
|
+
} catch (err) {
|
|
30388
|
+
console.error(
|
|
30389
|
+
`[schedulerController] Error computing next fire for ${task.taskId}:`,
|
|
30390
|
+
err,
|
|
30391
|
+
);
|
|
30392
|
+
}
|
|
30393
|
+
// Fallback: 1 hour from now
|
|
30394
|
+
return now + 3600000;
|
|
30395
|
+
}
|
|
30618
30396
|
|
|
30619
|
-
|
|
30620
|
-
|
|
30621
|
-
|
|
30622
|
-
loadRegistry() {
|
|
30623
|
-
try {
|
|
30624
|
-
if (fs.existsSync(REGISTRY_CONFIG_FILE)) {
|
|
30625
|
-
const data = fs.readFileSync(REGISTRY_CONFIG_FILE, "utf8");
|
|
30626
|
-
const registryData = JSON.parse(data);
|
|
30627
|
-
this.widgets = new Map(registryData.widgets || []);
|
|
30628
|
-
console.log(
|
|
30629
|
-
`[WidgetRegistry] Loaded ${this.widgets.size} widgets from cache`,
|
|
30630
|
-
);
|
|
30631
|
-
}
|
|
30632
|
-
} catch (error) {
|
|
30633
|
-
console.error("[WidgetRegistry] Error loading registry:", error);
|
|
30634
|
-
}
|
|
30635
|
-
}
|
|
30636
|
-
|
|
30637
|
-
/**
|
|
30638
|
-
* Save registry to disk
|
|
30639
|
-
*/
|
|
30640
|
-
saveRegistry() {
|
|
30641
|
-
try {
|
|
30642
|
-
const registryData = {
|
|
30643
|
-
lastUpdated: new Date().toISOString(),
|
|
30644
|
-
widgets: Array.from(this.widgets.entries()),
|
|
30645
|
-
};
|
|
30646
|
-
fs.writeFileSync(
|
|
30647
|
-
REGISTRY_CONFIG_FILE,
|
|
30648
|
-
JSON.stringify(registryData, null, 2),
|
|
30649
|
-
);
|
|
30650
|
-
} catch (error) {
|
|
30651
|
-
console.error("[WidgetRegistry] Error saving registry:", error);
|
|
30652
|
-
}
|
|
30653
|
-
}
|
|
30654
|
-
|
|
30655
|
-
/**
|
|
30656
|
-
* Resolve download URL from partial template or full URL
|
|
30657
|
-
* Supports placeholders: {version}, {name}
|
|
30658
|
-
*
|
|
30659
|
-
* Examples:
|
|
30660
|
-
* - Full URL: "https://github.com/user/widget/releases/download/v1.0.0/widget.zip"
|
|
30661
|
-
* - Template: "https://github.com/user/weather-widget/releases/download/v{version}/weather-widget.zip"
|
|
30662
|
-
* - Partial: "https://github.com/user/weather-widget/releases/download/" (auto-generates v{version}/{name}.zip)
|
|
30663
|
-
*
|
|
30664
|
-
* @param {string} urlTemplate - URL template or partial URL
|
|
30665
|
-
* @param {string} version - Widget version (e.g., "1.0.0")
|
|
30666
|
-
* @param {string} name - Widget name (e.g., "weather-widget")
|
|
30667
|
-
* @returns {string} Resolved download URL
|
|
30668
|
-
*/
|
|
30669
|
-
resolveDownloadUrl(urlTemplate, version, name) {
|
|
30670
|
-
if (!urlTemplate) return null;
|
|
30671
|
-
|
|
30672
|
-
if (urlTemplate.endsWith("/")) {
|
|
30673
|
-
return `${urlTemplate}v${version}/${name}.zip`;
|
|
30674
|
-
}
|
|
30675
|
-
|
|
30676
|
-
let url = urlTemplate;
|
|
30677
|
-
url = url.replace("{version}", version);
|
|
30678
|
-
url = url.replace("{name}", name);
|
|
30679
|
-
return url;
|
|
30680
|
-
}
|
|
30681
|
-
|
|
30682
|
-
/**
|
|
30683
|
-
* Determine if the input points to a local path (file:// or filesystem path)
|
|
30684
|
-
* @param {string} input - URL or path
|
|
30685
|
-
* @returns {boolean}
|
|
30686
|
-
*/
|
|
30687
|
-
isLocalSource(input) {
|
|
30688
|
-
if (!input) return false;
|
|
30689
|
-
if (input.startsWith("file://")) return true;
|
|
30690
|
-
if (input.startsWith("http://") || input.startsWith("https://"))
|
|
30691
|
-
return false;
|
|
30692
|
-
const resolvedPath = this.resolveLocalPath(input);
|
|
30693
|
-
return fs.existsSync(resolvedPath);
|
|
30694
|
-
}
|
|
30695
|
-
|
|
30696
|
-
/**
|
|
30697
|
-
* Normalize a local path (supports file:// and ~)
|
|
30698
|
-
* @param {string} input - Local path or file:// URL
|
|
30699
|
-
* @returns {string}
|
|
30700
|
-
*/
|
|
30701
|
-
resolveLocalPath(input) {
|
|
30702
|
-
if (input.startsWith("file://")) {
|
|
30703
|
-
return fileURLToPath(input);
|
|
30704
|
-
}
|
|
30705
|
-
if (input.startsWith("~")) {
|
|
30706
|
-
return path.join(os.homedir(), input.slice(1));
|
|
30707
|
-
}
|
|
30708
|
-
return path.resolve(input);
|
|
30709
|
-
}
|
|
30397
|
+
// Unknown schedule type — default 1 hour
|
|
30398
|
+
return now + 3600000;
|
|
30399
|
+
}
|
|
30710
30400
|
|
|
30711
|
-
|
|
30712
|
-
|
|
30713
|
-
|
|
30714
|
-
|
|
30715
|
-
|
|
30716
|
-
|
|
30717
|
-
|
|
30718
|
-
|
|
30719
|
-
|
|
30720
|
-
|
|
30721
|
-
|
|
30722
|
-
autoRegister = true,
|
|
30723
|
-
dashConfigPath = null,
|
|
30724
|
-
) {
|
|
30725
|
-
try {
|
|
30726
|
-
const resolvedPath = this.resolveLocalPath(localPath);
|
|
30401
|
+
/**
|
|
30402
|
+
* Fire a task: broadcast to renderer windows, queue pending, send notification if no windows.
|
|
30403
|
+
*/
|
|
30404
|
+
function fireTask(task) {
|
|
30405
|
+
const payload = {
|
|
30406
|
+
taskId: task.taskId,
|
|
30407
|
+
widgetId: task.widgetId,
|
|
30408
|
+
taskKey: task.taskKey,
|
|
30409
|
+
handler: task.handler,
|
|
30410
|
+
firedAt: Date.now(),
|
|
30411
|
+
};
|
|
30727
30412
|
|
|
30728
|
-
|
|
30729
|
-
|
|
30730
|
-
|
|
30413
|
+
console.log(
|
|
30414
|
+
`[schedulerController] Fired: ${task.widgetName}.${task.taskKey} (${task.displayName})`,
|
|
30415
|
+
);
|
|
30731
30416
|
|
|
30732
|
-
|
|
30417
|
+
// Add to pending results queue
|
|
30418
|
+
let queue = pendingResults.get(task.widgetId) || [];
|
|
30419
|
+
queue.push({
|
|
30420
|
+
taskId: task.taskId,
|
|
30421
|
+
taskKey: task.taskKey,
|
|
30422
|
+
firedAt: payload.firedAt,
|
|
30423
|
+
});
|
|
30424
|
+
if (queue.length > MAX_PENDING_PER_WIDGET) {
|
|
30425
|
+
queue = queue.slice(-MAX_PENDING_PER_WIDGET);
|
|
30426
|
+
}
|
|
30427
|
+
pendingResults.set(task.widgetId, queue);
|
|
30733
30428
|
|
|
30734
|
-
|
|
30735
|
-
|
|
30736
|
-
|
|
30429
|
+
// Broadcast to all windows
|
|
30430
|
+
const windows = deps.getWindows ? deps.getWindows() : [];
|
|
30431
|
+
if (windows.length > 0) {
|
|
30432
|
+
for (const win of windows) {
|
|
30433
|
+
if (!win.isDestroyed()) {
|
|
30434
|
+
win.webContents.send("scheduler:task-fired", payload);
|
|
30435
|
+
}
|
|
30436
|
+
}
|
|
30437
|
+
} else {
|
|
30438
|
+
// No windows open — send native OS notification
|
|
30439
|
+
if (deps.notificationController && deps.getMainWindow) {
|
|
30440
|
+
deps.notificationController.send(deps.getMainWindow(), {
|
|
30441
|
+
widgetName: task.widgetName,
|
|
30442
|
+
widgetId: task.widgetId,
|
|
30443
|
+
workspaceId: task.workspaceId || "",
|
|
30444
|
+
type: "scheduled-task",
|
|
30445
|
+
title: task.displayName || task.taskKey,
|
|
30446
|
+
body: `Scheduled task "${task.displayName}" fired`,
|
|
30447
|
+
silent: false,
|
|
30448
|
+
});
|
|
30449
|
+
}
|
|
30450
|
+
}
|
|
30451
|
+
}
|
|
30737
30452
|
|
|
30738
|
-
|
|
30739
|
-
|
|
30740
|
-
|
|
30741
|
-
|
|
30742
|
-
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
30746
|
-
|
|
30747
|
-
|
|
30453
|
+
/**
|
|
30454
|
+
* Main tick — runs every 1s, checks all enabled tasks.
|
|
30455
|
+
*/
|
|
30456
|
+
function tick() {
|
|
30457
|
+
const now = Date.now();
|
|
30458
|
+
for (const [, task] of tasks) {
|
|
30459
|
+
if (!task.enabled || !task.nextFireAt || task.nextFireAt > now) continue;
|
|
30460
|
+
fireTask(task);
|
|
30461
|
+
task.nextFireAt = computeNextFire(task, now);
|
|
30462
|
+
task.lastFiredAt = now;
|
|
30463
|
+
task.fireCount = (task.fireCount || 0) + 1;
|
|
30464
|
+
}
|
|
30465
|
+
debouncedPersist();
|
|
30466
|
+
}
|
|
30748
30467
|
|
|
30749
|
-
|
|
30468
|
+
/**
|
|
30469
|
+
* Persist tasks to electron-store (debounced).
|
|
30470
|
+
*/
|
|
30471
|
+
function debouncedPersist() {
|
|
30472
|
+
if (persistTimeout) return;
|
|
30473
|
+
persistTimeout = setTimeout(() => {
|
|
30474
|
+
persistTimeout = null;
|
|
30475
|
+
persistNow();
|
|
30476
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
30477
|
+
}
|
|
30750
30478
|
|
|
30751
|
-
|
|
30752
|
-
|
|
30753
|
-
|
|
30754
|
-
|
|
30755
|
-
|
|
30756
|
-
|
|
30757
|
-
|
|
30479
|
+
function persistNow() {
|
|
30480
|
+
try {
|
|
30481
|
+
const data = {};
|
|
30482
|
+
for (const [taskId, task] of tasks) {
|
|
30483
|
+
data[taskId] = { ...task };
|
|
30484
|
+
}
|
|
30485
|
+
store$2.set("tasks", data);
|
|
30486
|
+
} catch (err) {
|
|
30487
|
+
console.error("[schedulerController] Error persisting tasks:", err);
|
|
30488
|
+
}
|
|
30489
|
+
}
|
|
30758
30490
|
|
|
30759
|
-
|
|
30491
|
+
/**
|
|
30492
|
+
* Load persisted tasks from electron-store.
|
|
30493
|
+
*/
|
|
30494
|
+
function loadFromStore() {
|
|
30495
|
+
try {
|
|
30496
|
+
const data = store$2.get("tasks", {});
|
|
30497
|
+
const now = Date.now();
|
|
30498
|
+
for (const [taskId, task] of Object.entries(data)) {
|
|
30499
|
+
// Recompute nextFireAt if it's in the past
|
|
30500
|
+
if (task.nextFireAt && task.nextFireAt <= now && task.enabled) {
|
|
30501
|
+
task.nextFireAt = computeNextFire(task, now);
|
|
30502
|
+
}
|
|
30503
|
+
tasks.set(taskId, task);
|
|
30504
|
+
}
|
|
30505
|
+
console.log(`[schedulerController] Loaded ${tasks.size} tasks from store`);
|
|
30506
|
+
} catch (err) {
|
|
30507
|
+
console.error("[schedulerController] Error loading tasks:", err);
|
|
30508
|
+
}
|
|
30509
|
+
}
|
|
30760
30510
|
|
|
30761
|
-
|
|
30762
|
-
|
|
30763
|
-
|
|
30511
|
+
/**
|
|
30512
|
+
* Count tasks for a given widget instance.
|
|
30513
|
+
*/
|
|
30514
|
+
function countTasksForWidget(widgetId) {
|
|
30515
|
+
let count = 0;
|
|
30516
|
+
for (const [, task] of tasks) {
|
|
30517
|
+
if (task.widgetId === widgetId) count++;
|
|
30518
|
+
}
|
|
30519
|
+
return count;
|
|
30520
|
+
}
|
|
30764
30521
|
|
|
30765
|
-
|
|
30766
|
-
|
|
30767
|
-
|
|
30768
|
-
|
|
30769
|
-
|
|
30770
|
-
|
|
30771
|
-
|
|
30772
|
-
|
|
30773
|
-
|
|
30522
|
+
const schedulerController$2 = {
|
|
30523
|
+
/**
|
|
30524
|
+
* Wire dependencies from the Electron main process.
|
|
30525
|
+
*/
|
|
30526
|
+
init({ getWindows, notificationController, getMainWindow }) {
|
|
30527
|
+
deps.getWindows = getWindows;
|
|
30528
|
+
deps.notificationController = notificationController;
|
|
30529
|
+
deps.getMainWindow = getMainWindow;
|
|
30530
|
+
},
|
|
30774
30531
|
|
|
30775
|
-
|
|
30776
|
-
|
|
30777
|
-
|
|
30778
|
-
|
|
30779
|
-
|
|
30780
|
-
|
|
30781
|
-
|
|
30782
|
-
|
|
30783
|
-
|
|
30784
|
-
|
|
30785
|
-
if (fs.existsSync(path.join(dirPath, "dash.json"))) return true;
|
|
30532
|
+
/**
|
|
30533
|
+
* Start the tick loop and load persisted tasks.
|
|
30534
|
+
*/
|
|
30535
|
+
start() {
|
|
30536
|
+
loadFromStore();
|
|
30537
|
+
if (!tickInterval) {
|
|
30538
|
+
tickInterval = setInterval(tick, 1000);
|
|
30539
|
+
console.log("[schedulerController] Tick loop started");
|
|
30540
|
+
}
|
|
30541
|
+
},
|
|
30786
30542
|
|
|
30787
|
-
|
|
30788
|
-
|
|
30789
|
-
|
|
30790
|
-
|
|
30791
|
-
|
|
30543
|
+
/**
|
|
30544
|
+
* Stop the tick loop and persist immediately.
|
|
30545
|
+
*/
|
|
30546
|
+
stop() {
|
|
30547
|
+
if (tickInterval) {
|
|
30548
|
+
clearInterval(tickInterval);
|
|
30549
|
+
tickInterval = null;
|
|
30550
|
+
}
|
|
30551
|
+
if (persistTimeout) {
|
|
30552
|
+
clearTimeout(persistTimeout);
|
|
30553
|
+
persistTimeout = null;
|
|
30554
|
+
}
|
|
30555
|
+
persistNow();
|
|
30556
|
+
console.log("[schedulerController] Stopped");
|
|
30557
|
+
},
|
|
30792
30558
|
|
|
30793
|
-
|
|
30794
|
-
|
|
30559
|
+
/**
|
|
30560
|
+
* Register or update a scheduled task.
|
|
30561
|
+
*
|
|
30562
|
+
* @param {Object} payload
|
|
30563
|
+
* @param {string} payload.widgetId - widget instance UUID
|
|
30564
|
+
* @param {string} payload.widgetName - component name
|
|
30565
|
+
* @param {string} [payload.workspaceId]
|
|
30566
|
+
* @param {string} payload.taskKey - key from .dash.js
|
|
30567
|
+
* @param {string} payload.handler - handler function name
|
|
30568
|
+
* @param {string} payload.displayName - human-readable name
|
|
30569
|
+
* @param {string} payload.scheduleType - "interval" | "dayTime"
|
|
30570
|
+
* @param {number} [payload.intervalMs] - for interval type
|
|
30571
|
+
* @param {string[]} [payload.days] - for dayTime type
|
|
30572
|
+
* @param {string} [payload.time] - for dayTime type (HH:mm)
|
|
30573
|
+
* @param {boolean} [payload.enabled] - defaults to true
|
|
30574
|
+
* @returns {{ success: boolean, taskId?: string, error?: string }}
|
|
30575
|
+
*/
|
|
30576
|
+
registerTask(payload) {
|
|
30577
|
+
try {
|
|
30578
|
+
const {
|
|
30579
|
+
widgetId,
|
|
30580
|
+
widgetName,
|
|
30581
|
+
workspaceId,
|
|
30582
|
+
taskKey,
|
|
30583
|
+
handler,
|
|
30584
|
+
displayName,
|
|
30585
|
+
scheduleType,
|
|
30586
|
+
intervalMs,
|
|
30587
|
+
days,
|
|
30588
|
+
time,
|
|
30589
|
+
} = payload;
|
|
30795
30590
|
|
|
30796
|
-
|
|
30797
|
-
|
|
30798
|
-
*
|
|
30799
|
-
* Smart detection:
|
|
30800
|
-
* 1. If the selected folder itself is a widget, install it directly.
|
|
30801
|
-
* 2. Otherwise iterate subdirectories, skipping non-widget dirs.
|
|
30802
|
-
*
|
|
30803
|
-
* @param {string} folderPath - Path containing widget folders (or a single widget folder)
|
|
30804
|
-
* @param {boolean} autoRegister - Automatically register with ComponentManager
|
|
30805
|
-
* @returns {Promise<Array>} Registered widgets (with optional `mode` and `skipped` metadata)
|
|
30806
|
-
*/
|
|
30807
|
-
async registerWidgetsFromFolder(folderPath, autoRegister = true) {
|
|
30808
|
-
const SKIP_DIRS = new Set(["node_modules", "dist", "__MACOSX", ".git"]);
|
|
30591
|
+
const taskId = `${widgetId}:${taskKey}`;
|
|
30592
|
+
const existing = tasks.get(taskId);
|
|
30809
30593
|
|
|
30810
|
-
|
|
30811
|
-
|
|
30812
|
-
|
|
30813
|
-
|
|
30814
|
-
}
|
|
30815
|
-
if (!fs.statSync(resolvedPath).isDirectory()) {
|
|
30816
|
-
throw new Error(`Path is not a directory: ${resolvedPath}`);
|
|
30817
|
-
}
|
|
30594
|
+
// Rate limit: max tasks per widget
|
|
30595
|
+
if (!existing && countTasksForWidget(widgetId) >= MAX_TASKS_PER_WIDGET) {
|
|
30596
|
+
return { success: false, error: "max_tasks_reached" };
|
|
30597
|
+
}
|
|
30818
30598
|
|
|
30819
|
-
|
|
30820
|
-
|
|
30821
|
-
|
|
30822
|
-
|
|
30823
|
-
|
|
30824
|
-
|
|
30825
|
-
|
|
30826
|
-
|
|
30827
|
-
|
|
30828
|
-
|
|
30829
|
-
|
|
30830
|
-
|
|
30831
|
-
|
|
30832
|
-
|
|
30833
|
-
|
|
30834
|
-
|
|
30835
|
-
|
|
30599
|
+
const now = Date.now();
|
|
30600
|
+
const task = {
|
|
30601
|
+
taskId,
|
|
30602
|
+
widgetId,
|
|
30603
|
+
widgetName: widgetName || existing?.widgetName || "",
|
|
30604
|
+
workspaceId: workspaceId || existing?.workspaceId || "",
|
|
30605
|
+
taskKey,
|
|
30606
|
+
handler: handler || existing?.handler || taskKey,
|
|
30607
|
+
displayName: displayName || existing?.displayName || taskKey,
|
|
30608
|
+
scheduleType: scheduleType || existing?.scheduleType || "interval",
|
|
30609
|
+
intervalMs:
|
|
30610
|
+
intervalMs !== undefined ? intervalMs : existing?.intervalMs || null,
|
|
30611
|
+
days: days !== undefined ? days : existing?.days || null,
|
|
30612
|
+
time: time !== undefined ? time : existing?.time || null,
|
|
30613
|
+
enabled:
|
|
30614
|
+
payload.enabled !== undefined
|
|
30615
|
+
? payload.enabled
|
|
30616
|
+
: existing?.enabled !== undefined
|
|
30617
|
+
? existing.enabled
|
|
30618
|
+
: true,
|
|
30619
|
+
nextFireAt: 0,
|
|
30620
|
+
lastFiredAt: existing?.lastFiredAt || null,
|
|
30621
|
+
fireCount: existing?.fireCount || 0,
|
|
30622
|
+
createdAt: existing?.createdAt || new Date().toISOString(),
|
|
30623
|
+
};
|
|
30836
30624
|
|
|
30837
|
-
|
|
30838
|
-
|
|
30839
|
-
withFileTypes: true,
|
|
30840
|
-
});
|
|
30841
|
-
const results = [];
|
|
30842
|
-
let skipped = 0;
|
|
30625
|
+
// Compute next fire
|
|
30626
|
+
task.nextFireAt = task.enabled ? computeNextFire(task, now) : 0;
|
|
30843
30627
|
|
|
30844
|
-
|
|
30845
|
-
|
|
30628
|
+
tasks.set(taskId, task);
|
|
30629
|
+
debouncedPersist();
|
|
30846
30630
|
|
|
30847
|
-
|
|
30848
|
-
|
|
30849
|
-
|
|
30850
|
-
continue;
|
|
30851
|
-
}
|
|
30631
|
+
console.log(
|
|
30632
|
+
`[schedulerController] Registered: ${taskId} (${task.scheduleType})`,
|
|
30633
|
+
);
|
|
30852
30634
|
|
|
30853
|
-
|
|
30635
|
+
return { success: true, taskId };
|
|
30636
|
+
} catch (error) {
|
|
30637
|
+
console.error("[schedulerController] Error registering task:", error);
|
|
30638
|
+
return { success: false, error: error.message };
|
|
30639
|
+
}
|
|
30640
|
+
},
|
|
30854
30641
|
|
|
30855
|
-
|
|
30856
|
-
|
|
30857
|
-
|
|
30858
|
-
|
|
30642
|
+
/**
|
|
30643
|
+
* Remove a single task.
|
|
30644
|
+
* @param {string} taskId
|
|
30645
|
+
* @returns {{ success: boolean }}
|
|
30646
|
+
*/
|
|
30647
|
+
removeTask(taskId) {
|
|
30648
|
+
const deleted = tasks.delete(taskId);
|
|
30649
|
+
if (deleted) {
|
|
30650
|
+
debouncedPersist();
|
|
30651
|
+
console.log(`[schedulerController] Removed: ${taskId}`);
|
|
30652
|
+
}
|
|
30653
|
+
return { success: deleted };
|
|
30654
|
+
},
|
|
30859
30655
|
|
|
30860
|
-
|
|
30861
|
-
|
|
30656
|
+
/**
|
|
30657
|
+
* Remove all tasks for a widget instance.
|
|
30658
|
+
* @param {string} widgetId
|
|
30659
|
+
* @returns {{ success: boolean, count: number }}
|
|
30660
|
+
*/
|
|
30661
|
+
removeTasks(widgetId) {
|
|
30662
|
+
let count = 0;
|
|
30663
|
+
for (const [taskId, task] of tasks) {
|
|
30664
|
+
if (task.widgetId === widgetId) {
|
|
30665
|
+
tasks.delete(taskId);
|
|
30666
|
+
count++;
|
|
30667
|
+
}
|
|
30668
|
+
}
|
|
30669
|
+
if (count > 0) {
|
|
30670
|
+
debouncedPersist();
|
|
30671
|
+
pendingResults.delete(widgetId);
|
|
30672
|
+
}
|
|
30673
|
+
console.log(
|
|
30674
|
+
`[schedulerController] Removed ${count} tasks for widget ${widgetId}`,
|
|
30675
|
+
);
|
|
30676
|
+
return { success: true, count };
|
|
30677
|
+
},
|
|
30862
30678
|
|
|
30863
|
-
|
|
30864
|
-
|
|
30865
|
-
|
|
30679
|
+
/**
|
|
30680
|
+
* Get all tasks for a widget instance.
|
|
30681
|
+
* @param {string} widgetId
|
|
30682
|
+
* @returns {Object[]}
|
|
30683
|
+
*/
|
|
30684
|
+
getTasks(widgetId) {
|
|
30685
|
+
const result = [];
|
|
30686
|
+
for (const [, task] of tasks) {
|
|
30687
|
+
if (task.widgetId === widgetId) {
|
|
30688
|
+
result.push({ ...task });
|
|
30689
|
+
}
|
|
30690
|
+
}
|
|
30691
|
+
return result;
|
|
30692
|
+
},
|
|
30866
30693
|
|
|
30867
|
-
|
|
30868
|
-
|
|
30869
|
-
|
|
30870
|
-
|
|
30871
|
-
|
|
30872
|
-
|
|
30694
|
+
/**
|
|
30695
|
+
* Update a task's schedule configuration.
|
|
30696
|
+
* @param {string} taskId
|
|
30697
|
+
* @param {Object} updates
|
|
30698
|
+
* @returns {{ success: boolean }}
|
|
30699
|
+
*/
|
|
30700
|
+
updateTask(taskId, updates) {
|
|
30701
|
+
const task = tasks.get(taskId);
|
|
30702
|
+
if (!task) {
|
|
30703
|
+
return { success: false, error: "task_not_found" };
|
|
30704
|
+
}
|
|
30873
30705
|
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
|
|
30883
|
-
|
|
30884
|
-
|
|
30706
|
+
const allowedFields = [
|
|
30707
|
+
"scheduleType",
|
|
30708
|
+
"intervalMs",
|
|
30709
|
+
"days",
|
|
30710
|
+
"time",
|
|
30711
|
+
"displayName",
|
|
30712
|
+
"enabled",
|
|
30713
|
+
];
|
|
30714
|
+
for (const key of allowedFields) {
|
|
30715
|
+
if (updates[key] !== undefined) {
|
|
30716
|
+
task[key] = updates[key];
|
|
30717
|
+
}
|
|
30718
|
+
}
|
|
30885
30719
|
|
|
30886
|
-
|
|
30887
|
-
|
|
30888
|
-
|
|
30889
|
-
|
|
30890
|
-
|
|
30891
|
-
|
|
30892
|
-
* @returns {Promise<Object>} Widget configuration
|
|
30893
|
-
*/
|
|
30894
|
-
async downloadWidget(
|
|
30895
|
-
widgetName,
|
|
30896
|
-
downloadUrl,
|
|
30897
|
-
dashConfigUrl = null,
|
|
30898
|
-
autoRegister = true,
|
|
30899
|
-
) {
|
|
30900
|
-
try {
|
|
30901
|
-
if (this.isLocalSource(downloadUrl)) {
|
|
30902
|
-
return this.installFromLocalPath(
|
|
30903
|
-
widgetName,
|
|
30904
|
-
downloadUrl,
|
|
30905
|
-
autoRegister,
|
|
30906
|
-
dashConfigUrl,
|
|
30907
|
-
);
|
|
30908
|
-
}
|
|
30909
|
-
// Enforce HTTPS to prevent MITM attacks on widget downloads
|
|
30910
|
-
const parsedUrl = new URL(downloadUrl);
|
|
30911
|
-
if (parsedUrl.protocol !== "https:") {
|
|
30912
|
-
throw new Error(
|
|
30913
|
-
`Widget downloads must use HTTPS. Refusing to fetch: ${downloadUrl}`,
|
|
30914
|
-
);
|
|
30915
|
-
}
|
|
30720
|
+
// Recompute next fire
|
|
30721
|
+
if (task.enabled) {
|
|
30722
|
+
task.nextFireAt = computeNextFire(task, Date.now());
|
|
30723
|
+
} else {
|
|
30724
|
+
task.nextFireAt = 0;
|
|
30725
|
+
}
|
|
30916
30726
|
|
|
30917
|
-
|
|
30918
|
-
|
|
30919
|
-
|
|
30727
|
+
debouncedPersist();
|
|
30728
|
+
console.log(`[schedulerController] Updated: ${taskId}`);
|
|
30729
|
+
return { success: true };
|
|
30730
|
+
},
|
|
30920
30731
|
|
|
30921
|
-
|
|
30922
|
-
|
|
30923
|
-
|
|
30732
|
+
/**
|
|
30733
|
+
* Enable a task.
|
|
30734
|
+
* @param {string} taskId
|
|
30735
|
+
* @returns {{ success: boolean }}
|
|
30736
|
+
*/
|
|
30737
|
+
enableTask(taskId) {
|
|
30738
|
+
const task = tasks.get(taskId);
|
|
30739
|
+
if (!task) return { success: false, error: "task_not_found" };
|
|
30740
|
+
task.enabled = true;
|
|
30741
|
+
task.nextFireAt = computeNextFire(task, Date.now());
|
|
30742
|
+
debouncedPersist();
|
|
30743
|
+
console.log(`[schedulerController] Enabled: ${taskId}`);
|
|
30744
|
+
return { success: true };
|
|
30745
|
+
},
|
|
30924
30746
|
|
|
30925
|
-
|
|
30926
|
-
|
|
30747
|
+
/**
|
|
30748
|
+
* Disable a task.
|
|
30749
|
+
* @param {string} taskId
|
|
30750
|
+
* @returns {{ success: boolean }}
|
|
30751
|
+
*/
|
|
30752
|
+
disableTask(taskId) {
|
|
30753
|
+
const task = tasks.get(taskId);
|
|
30754
|
+
if (!task) return { success: false, error: "task_not_found" };
|
|
30755
|
+
task.enabled = false;
|
|
30756
|
+
task.nextFireAt = 0;
|
|
30757
|
+
debouncedPersist();
|
|
30758
|
+
console.log(`[schedulerController] Disabled: ${taskId}`);
|
|
30759
|
+
return { success: true };
|
|
30760
|
+
},
|
|
30927
30761
|
|
|
30928
|
-
|
|
30762
|
+
/**
|
|
30763
|
+
* Drain pending fire results for a widget.
|
|
30764
|
+
* @param {string} widgetId
|
|
30765
|
+
* @returns {Object[]}
|
|
30766
|
+
*/
|
|
30767
|
+
getPendingResults(widgetId) {
|
|
30768
|
+
const queue = pendingResults.get(widgetId) || [];
|
|
30769
|
+
pendingResults.delete(widgetId);
|
|
30770
|
+
return queue;
|
|
30771
|
+
},
|
|
30929
30772
|
|
|
30930
|
-
|
|
30931
|
-
|
|
30932
|
-
|
|
30773
|
+
/**
|
|
30774
|
+
* Handle system suspend — stop tick loop.
|
|
30775
|
+
*/
|
|
30776
|
+
handleSuspend() {
|
|
30777
|
+
if (tickInterval) {
|
|
30778
|
+
clearInterval(tickInterval);
|
|
30779
|
+
tickInterval = null;
|
|
30780
|
+
console.log("[schedulerController] Suspended (tick stopped)");
|
|
30781
|
+
}
|
|
30782
|
+
},
|
|
30933
30783
|
|
|
30934
|
-
|
|
30935
|
-
|
|
30936
|
-
|
|
30784
|
+
/**
|
|
30785
|
+
* Handle system resume — fire missed tasks, restart tick.
|
|
30786
|
+
*/
|
|
30787
|
+
handleResume() {
|
|
30788
|
+
const now = Date.now();
|
|
30789
|
+
for (const [, task] of tasks) {
|
|
30790
|
+
if (task.enabled && task.nextFireAt && task.nextFireAt <= now) {
|
|
30791
|
+
fireTask(task);
|
|
30792
|
+
task.nextFireAt = computeNextFire(task, now);
|
|
30793
|
+
task.lastFiredAt = now;
|
|
30794
|
+
task.fireCount = (task.fireCount || 0) + 1;
|
|
30795
|
+
}
|
|
30796
|
+
}
|
|
30797
|
+
debouncedPersist();
|
|
30798
|
+
if (!tickInterval) {
|
|
30799
|
+
tickInterval = setInterval(tick, 1000);
|
|
30800
|
+
console.log("[schedulerController] Resumed (tick restarted)");
|
|
30801
|
+
}
|
|
30802
|
+
},
|
|
30803
|
+
|
|
30804
|
+
/**
|
|
30805
|
+
* Remove all tasks for a widget name (used on widget uninstall).
|
|
30806
|
+
* @param {string} widgetName - component name
|
|
30807
|
+
*/
|
|
30808
|
+
cleanupWidget(widgetName) {
|
|
30809
|
+
let count = 0;
|
|
30810
|
+
for (const [taskId, task] of tasks) {
|
|
30811
|
+
if (task.widgetName === widgetName) {
|
|
30812
|
+
tasks.delete(taskId);
|
|
30813
|
+
pendingResults.delete(task.widgetId);
|
|
30814
|
+
count++;
|
|
30815
|
+
}
|
|
30816
|
+
}
|
|
30817
|
+
if (count > 0) {
|
|
30818
|
+
debouncedPersist();
|
|
30819
|
+
console.log(
|
|
30820
|
+
`[schedulerController] Cleaned up ${count} tasks for widget "${widgetName}"`,
|
|
30821
|
+
);
|
|
30822
|
+
}
|
|
30823
|
+
},
|
|
30824
|
+
};
|
|
30937
30825
|
|
|
30938
|
-
|
|
30826
|
+
var schedulerController_1 = schedulerController$2;
|
|
30939
30827
|
|
|
30940
|
-
|
|
30941
|
-
|
|
30942
|
-
|
|
30943
|
-
|
|
30828
|
+
/**
|
|
30829
|
+
* Widget Registry System
|
|
30830
|
+
*
|
|
30831
|
+
* Manages widget discovery, download, and dynamic loading
|
|
30832
|
+
* Widgets are expected to have:
|
|
30833
|
+
* - package.json (or dash.json) with metadata
|
|
30834
|
+
* - widgets/ folder containing [WidgetName].js and [WidgetName].dash.js
|
|
30835
|
+
*
|
|
30836
|
+
* Files are stored in the Electron app's userData directory:
|
|
30837
|
+
* - macOS: ~/Library/Application Support/[appName]/
|
|
30838
|
+
* - Windows: %APPDATA%/[appName]/
|
|
30839
|
+
* - Linux: ~/.config/[appName]/
|
|
30840
|
+
*/
|
|
30944
30841
|
|
|
30945
|
-
|
|
30842
|
+
(function (module) {
|
|
30843
|
+
const fs = require$$0$3;
|
|
30844
|
+
const path = require$$1$2;
|
|
30845
|
+
const os = require$$2$4;
|
|
30846
|
+
const AdmZip = require$$3$4;
|
|
30847
|
+
const { fileURLToPath } = require$$4$1;
|
|
30848
|
+
const { app, ipcMain, BrowserWindow } = require$$0$2;
|
|
30849
|
+
const { dynamicWidgetLoader } = dynamicWidgetLoaderExports;
|
|
30850
|
+
const { compileWidget, findWidgetsDir } = widgetCompiler$1;
|
|
30946
30851
|
|
|
30947
|
-
|
|
30948
|
-
|
|
30949
|
-
}
|
|
30852
|
+
let WIDGETS_CACHE_DIR = null;
|
|
30853
|
+
let REGISTRY_CONFIG_FILE = null;
|
|
30950
30854
|
|
|
30951
|
-
|
|
30952
|
-
|
|
30953
|
-
|
|
30954
|
-
|
|
30955
|
-
|
|
30855
|
+
/**
|
|
30856
|
+
* Validate ZIP entries to prevent path traversal attacks.
|
|
30857
|
+
* Rejects entries containing '..' segments or absolute paths that would
|
|
30858
|
+
* write outside the target extraction directory.
|
|
30859
|
+
* @param {AdmZip} zip - AdmZip instance
|
|
30860
|
+
* @param {string} targetDir - Intended extraction directory
|
|
30861
|
+
* @throws {Error} If any entry would escape the target directory
|
|
30862
|
+
*/
|
|
30863
|
+
function validateZipEntries(zip, targetDir) {
|
|
30864
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
30865
|
+
for (const entry of zip.getEntries()) {
|
|
30866
|
+
const entryPath = entry.entryName;
|
|
30867
|
+
// Reject entries with '..' path segments
|
|
30868
|
+
if (
|
|
30869
|
+
entryPath.split("/").includes("..") ||
|
|
30870
|
+
entryPath.split("\\").includes("..")
|
|
30871
|
+
) {
|
|
30872
|
+
throw new Error(
|
|
30873
|
+
`Malicious ZIP entry rejected (path traversal): ${entryPath}`,
|
|
30874
|
+
);
|
|
30875
|
+
}
|
|
30876
|
+
// Reject absolute paths
|
|
30877
|
+
if (path.isAbsolute(entryPath)) {
|
|
30878
|
+
throw new Error(
|
|
30879
|
+
`Malicious ZIP entry rejected (absolute path): ${entryPath}`,
|
|
30880
|
+
);
|
|
30881
|
+
}
|
|
30882
|
+
// Final check: resolved path must be within target directory
|
|
30883
|
+
const resolvedEntry = path.resolve(resolvedTarget, entryPath);
|
|
30884
|
+
if (
|
|
30885
|
+
!resolvedEntry.startsWith(resolvedTarget + path.sep) &&
|
|
30886
|
+
resolvedEntry !== resolvedTarget
|
|
30887
|
+
) {
|
|
30888
|
+
throw new Error(
|
|
30889
|
+
`Malicious ZIP entry rejected (escapes target): ${entryPath}`,
|
|
30956
30890
|
);
|
|
30957
|
-
throw error;
|
|
30958
30891
|
}
|
|
30959
30892
|
}
|
|
30893
|
+
}
|
|
30960
30894
|
|
|
30961
|
-
|
|
30962
|
-
|
|
30963
|
-
|
|
30964
|
-
|
|
30965
|
-
|
|
30966
|
-
|
|
30967
|
-
|
|
30968
|
-
|
|
30969
|
-
|
|
30970
|
-
|
|
30971
|
-
|
|
30972
|
-
|
|
30973
|
-
|
|
30974
|
-
|
|
30975
|
-
const packageJsonPath = path.join(widgetPath, "package.json");
|
|
30976
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
30977
|
-
const packageJson = JSON.parse(
|
|
30978
|
-
fs.readFileSync(packageJsonPath, "utf8"),
|
|
30979
|
-
);
|
|
30980
|
-
return {
|
|
30981
|
-
name: packageJson.name || widgetName,
|
|
30982
|
-
version: packageJson.version,
|
|
30983
|
-
description: packageJson.description,
|
|
30984
|
-
author: packageJson.author,
|
|
30985
|
-
repository: packageJson.repository,
|
|
30986
|
-
};
|
|
30987
|
-
}
|
|
30895
|
+
/**
|
|
30896
|
+
* Initialize registry with custom path or default userData path
|
|
30897
|
+
* @param {string} customPath - Optional custom path for storing widgets
|
|
30898
|
+
*/
|
|
30899
|
+
function initializeRegistry(customPath = null) {
|
|
30900
|
+
if (customPath) {
|
|
30901
|
+
WIDGETS_CACHE_DIR = path.join(customPath, "widgets");
|
|
30902
|
+
} else {
|
|
30903
|
+
WIDGETS_CACHE_DIR = path.join(app.getPath("userData"), "widgets");
|
|
30904
|
+
}
|
|
30905
|
+
REGISTRY_CONFIG_FILE = path.join(WIDGETS_CACHE_DIR, "registry.json");
|
|
30906
|
+
console.log(`[WidgetRegistry] Using storage path: ${WIDGETS_CACHE_DIR}`);
|
|
30907
|
+
}
|
|
30988
30908
|
|
|
30989
|
-
|
|
30990
|
-
|
|
30991
|
-
|
|
30992
|
-
|
|
30993
|
-
} catch (error) {
|
|
30994
|
-
console.error(
|
|
30995
|
-
`[WidgetRegistry] Error loading config for ${widgetName}:`,
|
|
30996
|
-
error,
|
|
30997
|
-
);
|
|
30998
|
-
return { name: widgetName };
|
|
30909
|
+
class WidgetRegistry {
|
|
30910
|
+
constructor(componentManager = null, customPath = null) {
|
|
30911
|
+
if (!WIDGETS_CACHE_DIR) {
|
|
30912
|
+
initializeRegistry(customPath);
|
|
30999
30913
|
}
|
|
30914
|
+
|
|
30915
|
+
this.widgets = new Map();
|
|
30916
|
+
this.componentManager = componentManager;
|
|
30917
|
+
this.ensureCacheDir();
|
|
30918
|
+
this.loadRegistry();
|
|
31000
30919
|
}
|
|
31001
30920
|
|
|
31002
30921
|
/**
|
|
31003
|
-
*
|
|
31004
|
-
*
|
|
31005
|
-
* @param {
|
|
31006
|
-
* @param {string} widgetPath - Path to widget directory
|
|
31007
|
-
* @param {boolean} autoRegister - Automatically register with ComponentManager
|
|
30922
|
+
* Static method to initialize registry with custom path
|
|
30923
|
+
* Call this early in your app startup (e.g., in main.js)
|
|
30924
|
+
* @param {string} customPath - Custom path for storing widgets/configs
|
|
31008
30925
|
*/
|
|
31009
|
-
|
|
31010
|
-
|
|
31011
|
-
name: widgetName,
|
|
31012
|
-
path: widgetPath,
|
|
31013
|
-
...config,
|
|
31014
|
-
registeredAt: new Date().toISOString(),
|
|
31015
|
-
};
|
|
31016
|
-
|
|
31017
|
-
this.widgets.set(widgetName, widgetEntry);
|
|
31018
|
-
this.saveRegistry();
|
|
31019
|
-
console.log(`[WidgetRegistry] Registered widget: ${widgetName}`);
|
|
30926
|
+
static initialize(customPath = null) {
|
|
30927
|
+
initializeRegistry(customPath);
|
|
31020
30928
|
}
|
|
31021
30929
|
|
|
31022
30930
|
/**
|
|
31023
|
-
*
|
|
31024
|
-
* @param {
|
|
31025
|
-
* @param {string} widgetPath - Path to widget directory
|
|
30931
|
+
* Set ComponentManager instance for automatic widget registration
|
|
30932
|
+
* @param {Object} manager - ComponentManager instance from @trops/dash-react
|
|
31026
30933
|
*/
|
|
31027
|
-
|
|
31028
|
-
|
|
31029
|
-
|
|
31030
|
-
if (!findBundlePath(widgetPath)) {
|
|
31031
|
-
try {
|
|
31032
|
-
await compileWidget(widgetPath);
|
|
31033
|
-
console.log(`[WidgetRegistry] Auto-compiled ${widgetName}`);
|
|
31034
|
-
} catch (compileError) {
|
|
31035
|
-
console.warn(
|
|
31036
|
-
`[WidgetRegistry] Could not compile ${widgetName}:`,
|
|
31037
|
-
compileError,
|
|
31038
|
-
);
|
|
31039
|
-
}
|
|
31040
|
-
}
|
|
31041
|
-
|
|
31042
|
-
if (this.componentManager) {
|
|
31043
|
-
dynamicWidgetLoader.setComponentManager(this.componentManager);
|
|
31044
|
-
}
|
|
31045
|
-
|
|
31046
|
-
const components = dynamicWidgetLoader.discoverWidgets(widgetPath);
|
|
31047
|
-
console.log(
|
|
31048
|
-
`[WidgetRegistry] Found ${components.length} components in ${widgetName}`,
|
|
31049
|
-
);
|
|
31050
|
-
|
|
31051
|
-
const existingEntry = this.widgets.get(widgetName);
|
|
31052
|
-
let registryUpdated = false;
|
|
31053
|
-
|
|
31054
|
-
// Store component names as displayName on the registry entry
|
|
31055
|
-
// so settings UI shows "WeatherWidget" instead of "weather-widget"
|
|
31056
|
-
if (components.length > 0 && existingEntry) {
|
|
31057
|
-
existingEntry.displayName = components.join(", ");
|
|
31058
|
-
existingEntry.componentNames = components;
|
|
31059
|
-
registryUpdated = true;
|
|
31060
|
-
}
|
|
31061
|
-
|
|
31062
|
-
for (const componentName of components) {
|
|
31063
|
-
try {
|
|
31064
|
-
const result = await dynamicWidgetLoader.loadWidget(
|
|
31065
|
-
widgetName,
|
|
31066
|
-
widgetPath,
|
|
31067
|
-
componentName,
|
|
31068
|
-
true,
|
|
31069
|
-
);
|
|
31070
|
-
console.log(`[WidgetRegistry] ✓ Loaded ${componentName}`);
|
|
30934
|
+
setComponentManager(manager) {
|
|
30935
|
+
this.componentManager = manager;
|
|
30936
|
+
}
|
|
31071
30937
|
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
|
|
31075
|
-
|
|
31076
|
-
|
|
31077
|
-
|
|
31078
|
-
|
|
31079
|
-
|
|
31080
|
-
if (cfg.workspace && !existingEntry.workspace)
|
|
31081
|
-
existingEntry.workspace = cfg.workspace;
|
|
31082
|
-
if (cfg.events?.length && !existingEntry.events?.length)
|
|
31083
|
-
existingEntry.events = cfg.events;
|
|
31084
|
-
if (
|
|
31085
|
-
cfg.eventHandlers?.length &&
|
|
31086
|
-
!existingEntry.eventHandlers?.length
|
|
31087
|
-
)
|
|
31088
|
-
existingEntry.eventHandlers = cfg.eventHandlers;
|
|
31089
|
-
registryUpdated = true;
|
|
31090
|
-
}
|
|
31091
|
-
} catch (error) {
|
|
31092
|
-
console.error(
|
|
31093
|
-
`[WidgetRegistry] Error loading component ${componentName}:`,
|
|
31094
|
-
error,
|
|
31095
|
-
);
|
|
31096
|
-
}
|
|
31097
|
-
}
|
|
30938
|
+
/**
|
|
30939
|
+
* Ensure cache directory exists
|
|
30940
|
+
*/
|
|
30941
|
+
ensureCacheDir() {
|
|
30942
|
+
if (!fs.existsSync(WIDGETS_CACHE_DIR)) {
|
|
30943
|
+
fs.mkdirSync(WIDGETS_CACHE_DIR, { recursive: true });
|
|
30944
|
+
}
|
|
30945
|
+
}
|
|
31098
30946
|
|
|
31099
|
-
|
|
31100
|
-
|
|
31101
|
-
|
|
30947
|
+
/**
|
|
30948
|
+
* Load registry from disk
|
|
30949
|
+
*/
|
|
30950
|
+
loadRegistry() {
|
|
30951
|
+
try {
|
|
30952
|
+
if (fs.existsSync(REGISTRY_CONFIG_FILE)) {
|
|
30953
|
+
const data = fs.readFileSync(REGISTRY_CONFIG_FILE, "utf8");
|
|
30954
|
+
const registryData = JSON.parse(data);
|
|
30955
|
+
this.widgets = new Map(registryData.widgets || []);
|
|
30956
|
+
console.log(
|
|
30957
|
+
`[WidgetRegistry] Loaded ${this.widgets.size} widgets from cache`,
|
|
30958
|
+
);
|
|
31102
30959
|
}
|
|
31103
30960
|
} catch (error) {
|
|
31104
|
-
console.error("[WidgetRegistry] Error loading
|
|
30961
|
+
console.error("[WidgetRegistry] Error loading registry:", error);
|
|
31105
30962
|
}
|
|
31106
30963
|
}
|
|
31107
30964
|
|
|
31108
30965
|
/**
|
|
31109
|
-
*
|
|
31110
|
-
* @returns {Array} List of widget configurations
|
|
30966
|
+
* Save registry to disk
|
|
31111
30967
|
*/
|
|
31112
|
-
|
|
31113
|
-
|
|
30968
|
+
saveRegistry() {
|
|
30969
|
+
try {
|
|
30970
|
+
const registryData = {
|
|
30971
|
+
lastUpdated: new Date().toISOString(),
|
|
30972
|
+
widgets: Array.from(this.widgets.entries()),
|
|
30973
|
+
};
|
|
30974
|
+
fs.writeFileSync(
|
|
30975
|
+
REGISTRY_CONFIG_FILE,
|
|
30976
|
+
JSON.stringify(registryData, null, 2),
|
|
30977
|
+
);
|
|
30978
|
+
} catch (error) {
|
|
30979
|
+
console.error("[WidgetRegistry] Error saving registry:", error);
|
|
30980
|
+
}
|
|
31114
30981
|
}
|
|
31115
30982
|
|
|
31116
30983
|
/**
|
|
31117
|
-
*
|
|
31118
|
-
*
|
|
31119
|
-
*
|
|
30984
|
+
* Resolve download URL from partial template or full URL
|
|
30985
|
+
* Supports placeholders: {version}, {name}
|
|
30986
|
+
*
|
|
30987
|
+
* Examples:
|
|
30988
|
+
* - Full URL: "https://github.com/user/widget/releases/download/v1.0.0/widget.zip"
|
|
30989
|
+
* - Template: "https://github.com/user/weather-widget/releases/download/v{version}/weather-widget.zip"
|
|
30990
|
+
* - Partial: "https://github.com/user/weather-widget/releases/download/" (auto-generates v{version}/{name}.zip)
|
|
30991
|
+
*
|
|
30992
|
+
* @param {string} urlTemplate - URL template or partial URL
|
|
30993
|
+
* @param {string} version - Widget version (e.g., "1.0.0")
|
|
30994
|
+
* @param {string} name - Widget name (e.g., "weather-widget")
|
|
30995
|
+
* @returns {string} Resolved download URL
|
|
31120
30996
|
*/
|
|
31121
|
-
|
|
31122
|
-
|
|
30997
|
+
resolveDownloadUrl(urlTemplate, version, name) {
|
|
30998
|
+
if (!urlTemplate) return null;
|
|
30999
|
+
|
|
31000
|
+
if (urlTemplate.endsWith("/")) {
|
|
31001
|
+
return `${urlTemplate}v${version}/${name}.zip`;
|
|
31002
|
+
}
|
|
31003
|
+
|
|
31004
|
+
let url = urlTemplate;
|
|
31005
|
+
url = url.replace("{version}", version);
|
|
31006
|
+
url = url.replace("{name}", name);
|
|
31007
|
+
return url;
|
|
31123
31008
|
}
|
|
31124
31009
|
|
|
31125
31010
|
/**
|
|
31126
|
-
*
|
|
31127
|
-
* @param {string}
|
|
31011
|
+
* Determine if the input points to a local path (file:// or filesystem path)
|
|
31012
|
+
* @param {string} input - URL or path
|
|
31013
|
+
* @returns {boolean}
|
|
31128
31014
|
*/
|
|
31129
|
-
|
|
31130
|
-
|
|
31131
|
-
if (
|
|
31132
|
-
|
|
31015
|
+
isLocalSource(input) {
|
|
31016
|
+
if (!input) return false;
|
|
31017
|
+
if (input.startsWith("file://")) return true;
|
|
31018
|
+
if (input.startsWith("http://") || input.startsWith("https://"))
|
|
31133
31019
|
return false;
|
|
31020
|
+
const resolvedPath = this.resolveLocalPath(input);
|
|
31021
|
+
return fs.existsSync(resolvedPath);
|
|
31022
|
+
}
|
|
31023
|
+
|
|
31024
|
+
/**
|
|
31025
|
+
* Normalize a local path (supports file:// and ~)
|
|
31026
|
+
* @param {string} input - Local path or file:// URL
|
|
31027
|
+
* @returns {string}
|
|
31028
|
+
*/
|
|
31029
|
+
resolveLocalPath(input) {
|
|
31030
|
+
if (input.startsWith("file://")) {
|
|
31031
|
+
return fileURLToPath(input);
|
|
31032
|
+
}
|
|
31033
|
+
if (input.startsWith("~")) {
|
|
31034
|
+
return path.join(os.homedir(), input.slice(1));
|
|
31134
31035
|
}
|
|
31036
|
+
return path.resolve(input);
|
|
31037
|
+
}
|
|
31135
31038
|
|
|
31039
|
+
/**
|
|
31040
|
+
* Install a widget from a local ZIP file or folder path
|
|
31041
|
+
* @param {string} widgetName - Name of the widget
|
|
31042
|
+
* @param {string} localPath - Path to ZIP file or widget folder
|
|
31043
|
+
* @param {boolean} autoRegister - Automatically register with ComponentManager
|
|
31044
|
+
* @param {string} dashConfigPath - Optional: path to dash.json metadata file
|
|
31045
|
+
* @returns {Promise<Object>} Widget configuration
|
|
31046
|
+
*/
|
|
31047
|
+
async installFromLocalPath(
|
|
31048
|
+
widgetName,
|
|
31049
|
+
localPath,
|
|
31050
|
+
autoRegister = true,
|
|
31051
|
+
dashConfigPath = null,
|
|
31052
|
+
) {
|
|
31136
31053
|
try {
|
|
31137
|
-
|
|
31138
|
-
|
|
31054
|
+
const resolvedPath = this.resolveLocalPath(localPath);
|
|
31055
|
+
|
|
31056
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
31057
|
+
throw new Error(`Local path not found: ${resolvedPath}`);
|
|
31139
31058
|
}
|
|
31140
|
-
|
|
31141
|
-
|
|
31142
|
-
|
|
31143
|
-
|
|
31059
|
+
|
|
31060
|
+
const widgetPath = path.join(WIDGETS_CACHE_DIR, widgetName);
|
|
31061
|
+
|
|
31062
|
+
if (fs.existsSync(widgetPath)) {
|
|
31063
|
+
fs.rmSync(widgetPath, { recursive: true });
|
|
31064
|
+
}
|
|
31065
|
+
|
|
31066
|
+
const isDirectory = fs.statSync(resolvedPath).isDirectory();
|
|
31067
|
+
if (isDirectory) {
|
|
31068
|
+
fs.cpSync(resolvedPath, widgetPath, { recursive: true });
|
|
31069
|
+
} else if (resolvedPath.endsWith(".zip")) {
|
|
31070
|
+
const zip = new AdmZip(resolvedPath);
|
|
31071
|
+
validateZipEntries(zip, widgetPath);
|
|
31072
|
+
zip.extractAllTo(widgetPath, true);
|
|
31073
|
+
} else {
|
|
31074
|
+
throw new Error(`Unsupported local source type: ${resolvedPath}`);
|
|
31075
|
+
}
|
|
31076
|
+
|
|
31077
|
+
let config = await this.loadWidgetConfig(widgetName, widgetPath);
|
|
31078
|
+
|
|
31079
|
+
if (dashConfigPath) {
|
|
31080
|
+
const configPath = this.resolveLocalPath(dashConfigPath);
|
|
31081
|
+
if (fs.existsSync(configPath)) {
|
|
31082
|
+
const dashConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
31083
|
+
config = { ...config, ...dashConfig };
|
|
31084
|
+
}
|
|
31085
|
+
}
|
|
31086
|
+
|
|
31087
|
+
this.registerWidget(widgetName, config, widgetPath, false);
|
|
31088
|
+
|
|
31089
|
+
if (autoRegister) {
|
|
31090
|
+
await this.loadWidgetComponents(widgetName, widgetPath);
|
|
31091
|
+
}
|
|
31092
|
+
|
|
31093
|
+
return config;
|
|
31144
31094
|
} catch (error) {
|
|
31145
31095
|
console.error(
|
|
31146
|
-
`[WidgetRegistry] Error
|
|
31096
|
+
`[WidgetRegistry] Error installing local widget ${widgetName}:`,
|
|
31147
31097
|
error,
|
|
31148
31098
|
);
|
|
31149
|
-
|
|
31099
|
+
throw error;
|
|
31150
31100
|
}
|
|
31151
31101
|
}
|
|
31152
31102
|
|
|
31153
31103
|
/**
|
|
31154
|
-
*
|
|
31104
|
+
* Check if a directory looks like a valid widget folder.
|
|
31105
|
+
* A directory is a widget if it has:
|
|
31106
|
+
* - package.json or dash.json at its root, OR
|
|
31107
|
+
* - A widgets/ subdirectory containing at least one .dash.js file
|
|
31108
|
+
* @param {string} dirPath - Path to the directory
|
|
31109
|
+
* @returns {boolean}
|
|
31155
31110
|
*/
|
|
31156
|
-
|
|
31157
|
-
|
|
31158
|
-
if (
|
|
31159
|
-
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
31160
|
-
return response.json();
|
|
31161
|
-
}
|
|
31111
|
+
isWidgetFolder(dirPath) {
|
|
31112
|
+
if (fs.existsSync(path.join(dirPath, "package.json"))) return true;
|
|
31113
|
+
if (fs.existsSync(path.join(dirPath, "dash.json"))) return true;
|
|
31162
31114
|
|
|
31163
|
-
|
|
31164
|
-
|
|
31165
|
-
|
|
31166
|
-
|
|
31167
|
-
|
|
31115
|
+
const widgetsDir = path.join(dirPath, "widgets");
|
|
31116
|
+
if (fs.existsSync(widgetsDir) && fs.statSync(widgetsDir).isDirectory()) {
|
|
31117
|
+
const files = fs.readdirSync(widgetsDir);
|
|
31118
|
+
if (files.some((f) => f.endsWith(".dash.js"))) return true;
|
|
31119
|
+
}
|
|
31120
|
+
|
|
31121
|
+
return false;
|
|
31168
31122
|
}
|
|
31169
31123
|
|
|
31170
31124
|
/**
|
|
31171
|
-
*
|
|
31172
|
-
*
|
|
31125
|
+
* Register all widgets found in a local folder.
|
|
31126
|
+
*
|
|
31127
|
+
* Smart detection:
|
|
31128
|
+
* 1. If the selected folder itself is a widget, install it directly.
|
|
31129
|
+
* 2. Otherwise iterate subdirectories, skipping non-widget dirs.
|
|
31130
|
+
*
|
|
31131
|
+
* @param {string} folderPath - Path containing widget folders (or a single widget folder)
|
|
31132
|
+
* @param {boolean} autoRegister - Automatically register with ComponentManager
|
|
31133
|
+
* @returns {Promise<Array>} Registered widgets (with optional `mode` and `skipped` metadata)
|
|
31173
31134
|
*/
|
|
31174
|
-
|
|
31175
|
-
|
|
31176
|
-
}
|
|
31177
|
-
}
|
|
31135
|
+
async registerWidgetsFromFolder(folderPath, autoRegister = true) {
|
|
31136
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", "__MACOSX", ".git"]);
|
|
31178
31137
|
|
|
31179
|
-
|
|
31180
|
-
|
|
31138
|
+
try {
|
|
31139
|
+
const resolvedPath = this.resolveLocalPath(folderPath);
|
|
31140
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
31141
|
+
throw new Error(`Folder not found: ${resolvedPath}`);
|
|
31142
|
+
}
|
|
31143
|
+
if (!fs.statSync(resolvedPath).isDirectory()) {
|
|
31144
|
+
throw new Error(`Path is not a directory: ${resolvedPath}`);
|
|
31145
|
+
}
|
|
31181
31146
|
|
|
31182
|
-
|
|
31183
|
-
|
|
31184
|
-
|
|
31185
|
-
|
|
31186
|
-
|
|
31187
|
-
|
|
31147
|
+
// 1. Check if the selected folder itself is a widget
|
|
31148
|
+
if (this.isWidgetFolder(resolvedPath)) {
|
|
31149
|
+
const widgetName = path.basename(resolvedPath);
|
|
31150
|
+
const config = await this.installFromLocalPath(
|
|
31151
|
+
widgetName,
|
|
31152
|
+
resolvedPath,
|
|
31153
|
+
autoRegister,
|
|
31154
|
+
);
|
|
31155
|
+
return [
|
|
31156
|
+
{
|
|
31157
|
+
name: widgetName,
|
|
31158
|
+
path: resolvedPath,
|
|
31159
|
+
...config,
|
|
31160
|
+
mode: "single",
|
|
31161
|
+
},
|
|
31162
|
+
];
|
|
31163
|
+
}
|
|
31188
31164
|
|
|
31189
|
-
|
|
31190
|
-
|
|
31191
|
-
|
|
31192
|
-
|
|
31193
|
-
|
|
31194
|
-
|
|
31195
|
-
* @returns {string|null} Absolute path to the bundle, or null if not found
|
|
31196
|
-
*/
|
|
31197
|
-
function findBundlePath(widgetPath) {
|
|
31198
|
-
const candidates = [
|
|
31199
|
-
path.join(widgetPath, "dist", "index.cjs.js"),
|
|
31200
|
-
path.join(widgetPath, "index.cjs.js"),
|
|
31201
|
-
path.join(widgetPath, "dist", "index.js"),
|
|
31202
|
-
path.join(widgetPath, "index.js"),
|
|
31203
|
-
];
|
|
31165
|
+
// 2. Iterate subdirectories with filtering
|
|
31166
|
+
const entries = fs.readdirSync(resolvedPath, {
|
|
31167
|
+
withFileTypes: true,
|
|
31168
|
+
});
|
|
31169
|
+
const results = [];
|
|
31170
|
+
let skipped = 0;
|
|
31204
31171
|
|
|
31205
|
-
|
|
31206
|
-
|
|
31207
|
-
|
|
31208
|
-
|
|
31209
|
-
|
|
31210
|
-
|
|
31211
|
-
|
|
31212
|
-
console.log(`[WidgetRegistry] Skipping ESM bundle: ${candidate}`);
|
|
31213
|
-
continue;
|
|
31214
|
-
}
|
|
31215
|
-
} catch (_) {
|
|
31216
|
-
// Non-fatal — allow fallthrough
|
|
31172
|
+
for (const entry of entries) {
|
|
31173
|
+
if (!entry.isDirectory()) continue;
|
|
31174
|
+
|
|
31175
|
+
// Skip hidden dirs and known non-widget dirs
|
|
31176
|
+
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) {
|
|
31177
|
+
skipped++;
|
|
31178
|
+
continue;
|
|
31217
31179
|
}
|
|
31218
|
-
}
|
|
31219
|
-
return candidate;
|
|
31220
|
-
}
|
|
31221
|
-
}
|
|
31222
31180
|
|
|
31223
|
-
|
|
31224
|
-
}
|
|
31181
|
+
const widgetPath = path.join(resolvedPath, entry.name);
|
|
31225
31182
|
|
|
31226
|
-
|
|
31227
|
-
|
|
31228
|
-
|
|
31229
|
-
|
|
31230
|
-
ipcMain.handle("widget:list", () => getWidgetRegistry().getWidgets());
|
|
31183
|
+
if (!this.isWidgetFolder(widgetPath)) {
|
|
31184
|
+
skipped++;
|
|
31185
|
+
continue;
|
|
31186
|
+
}
|
|
31231
31187
|
|
|
31232
|
-
|
|
31233
|
-
|
|
31234
|
-
});
|
|
31188
|
+
const config = await this.loadWidgetConfig(entry.name, widgetPath);
|
|
31189
|
+
this.registerWidget(entry.name, config, widgetPath, false);
|
|
31235
31190
|
|
|
31236
|
-
|
|
31237
|
-
|
|
31238
|
-
|
|
31239
|
-
const config = await getWidgetRegistry().downloadWidget(
|
|
31240
|
-
widgetName,
|
|
31241
|
-
downloadUrl,
|
|
31242
|
-
dashConfigUrl,
|
|
31243
|
-
);
|
|
31191
|
+
if (autoRegister) {
|
|
31192
|
+
await this.loadWidgetComponents(entry.name, widgetPath);
|
|
31193
|
+
}
|
|
31244
31194
|
|
|
31245
|
-
|
|
31246
|
-
|
|
31247
|
-
|
|
31248
|
-
config,
|
|
31195
|
+
results.push({
|
|
31196
|
+
name: entry.name,
|
|
31197
|
+
path: widgetPath,
|
|
31198
|
+
...config,
|
|
31249
31199
|
});
|
|
31250
|
-
}
|
|
31251
|
-
|
|
31252
|
-
return config;
|
|
31253
|
-
},
|
|
31254
|
-
);
|
|
31200
|
+
}
|
|
31255
31201
|
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
|
|
31259
|
-
|
|
31260
|
-
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
dashConfigPath,
|
|
31202
|
+
// Attach skipped count as metadata on the array
|
|
31203
|
+
results.skipped = skipped;
|
|
31204
|
+
return results;
|
|
31205
|
+
} catch (error) {
|
|
31206
|
+
console.error(
|
|
31207
|
+
"[WidgetRegistry] Error registering widgets from folder:",
|
|
31208
|
+
error,
|
|
31264
31209
|
);
|
|
31210
|
+
throw error;
|
|
31211
|
+
}
|
|
31212
|
+
}
|
|
31265
31213
|
|
|
31266
|
-
|
|
31267
|
-
|
|
31214
|
+
/**
|
|
31215
|
+
* Download widget from URL (ZIP file)
|
|
31216
|
+
* @param {string} widgetName - Name of the widget
|
|
31217
|
+
* @param {string} downloadUrl - URL to download ZIP file from (supports templates and partial URLs)
|
|
31218
|
+
* @param {string} dashConfigUrl - Optional: URL to dash.json metadata file
|
|
31219
|
+
* @param {boolean} autoRegister - Automatically register with ComponentManager
|
|
31220
|
+
* @returns {Promise<Object>} Widget configuration
|
|
31221
|
+
*/
|
|
31222
|
+
async downloadWidget(
|
|
31223
|
+
widgetName,
|
|
31224
|
+
downloadUrl,
|
|
31225
|
+
dashConfigUrl = null,
|
|
31226
|
+
autoRegister = true,
|
|
31227
|
+
) {
|
|
31228
|
+
try {
|
|
31229
|
+
if (this.isLocalSource(downloadUrl)) {
|
|
31230
|
+
return this.installFromLocalPath(
|
|
31268
31231
|
widgetName,
|
|
31269
|
-
|
|
31270
|
-
|
|
31271
|
-
|
|
31232
|
+
downloadUrl,
|
|
31233
|
+
autoRegister,
|
|
31234
|
+
dashConfigUrl,
|
|
31235
|
+
);
|
|
31236
|
+
}
|
|
31237
|
+
// Enforce HTTPS to prevent MITM attacks on widget downloads
|
|
31238
|
+
const parsedUrl = new URL(downloadUrl);
|
|
31239
|
+
if (parsedUrl.protocol !== "https:") {
|
|
31240
|
+
throw new Error(
|
|
31241
|
+
`Widget downloads must use HTTPS. Refusing to fetch: ${downloadUrl}`,
|
|
31242
|
+
);
|
|
31243
|
+
}
|
|
31272
31244
|
|
|
31273
|
-
|
|
31274
|
-
|
|
31275
|
-
|
|
31245
|
+
console.log(
|
|
31246
|
+
`[WidgetRegistry] Downloading widget: ${widgetName} from ${downloadUrl}`,
|
|
31247
|
+
);
|
|
31276
31248
|
|
|
31277
|
-
|
|
31278
|
-
|
|
31279
|
-
|
|
31280
|
-
true,
|
|
31281
|
-
);
|
|
31249
|
+
const response = await fetch(downloadUrl);
|
|
31250
|
+
if (!response.ok)
|
|
31251
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
31282
31252
|
|
|
31283
|
-
|
|
31284
|
-
|
|
31285
|
-
count: results.length,
|
|
31286
|
-
widgets: results,
|
|
31287
|
-
});
|
|
31288
|
-
});
|
|
31253
|
+
const buffer = await response.arrayBuffer();
|
|
31254
|
+
const zip = new AdmZip(Buffer.from(buffer));
|
|
31289
31255
|
|
|
31290
|
-
|
|
31291
|
-
});
|
|
31256
|
+
const widgetPath = path.join(WIDGETS_CACHE_DIR, widgetName);
|
|
31292
31257
|
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31296
|
-
return getWidgetRegistry().uninstallWidget(widgetName);
|
|
31297
|
-
});
|
|
31258
|
+
if (fs.existsSync(widgetPath)) {
|
|
31259
|
+
fs.rmSync(widgetPath, { recursive: true });
|
|
31260
|
+
}
|
|
31298
31261
|
|
|
31299
|
-
|
|
31262
|
+
validateZipEntries(zip, widgetPath);
|
|
31263
|
+
zip.extractAllTo(widgetPath, true);
|
|
31264
|
+
console.log(`[WidgetRegistry] Extracted widget to: ${widgetPath}`);
|
|
31300
31265
|
|
|
31301
|
-
|
|
31302
|
-
getWidgetRegistry().getStoragePath(),
|
|
31303
|
-
);
|
|
31266
|
+
let config = await this.loadWidgetConfig(widgetName, widgetPath);
|
|
31304
31267
|
|
|
31305
|
-
|
|
31306
|
-
|
|
31307
|
-
|
|
31308
|
-
|
|
31309
|
-
const configs = [];
|
|
31268
|
+
if (dashConfigUrl) {
|
|
31269
|
+
const dashConfig = await this.fetchJSON(dashConfigUrl);
|
|
31270
|
+
config = { ...config, ...dashConfig };
|
|
31271
|
+
}
|
|
31310
31272
|
|
|
31311
|
-
|
|
31312
|
-
const widgetPath = widget.path;
|
|
31313
|
-
if (!widgetPath || !fs.existsSync(widgetPath)) continue;
|
|
31273
|
+
this.registerWidget(widgetName, config, widgetPath, false);
|
|
31314
31274
|
|
|
31315
|
-
|
|
31316
|
-
|
|
31317
|
-
for (const componentName of componentNames) {
|
|
31318
|
-
try {
|
|
31319
|
-
const configPath = path.join(
|
|
31320
|
-
widgetsDir || path.join(widgetPath, "widgets"),
|
|
31321
|
-
`${componentName}.dash.js`,
|
|
31322
|
-
);
|
|
31323
|
-
const config = await dynamicWidgetLoader.loadConfigFile(configPath);
|
|
31324
|
-
configs.push({
|
|
31325
|
-
componentName,
|
|
31326
|
-
widgetPackage: widget.name,
|
|
31327
|
-
// Include scoped id if present in the config
|
|
31328
|
-
id: config.id || null,
|
|
31329
|
-
config,
|
|
31330
|
-
});
|
|
31331
|
-
} catch (err) {
|
|
31332
|
-
console.error(
|
|
31333
|
-
`[WidgetRegistry] Error loading config for ${componentName}:`,
|
|
31334
|
-
err,
|
|
31335
|
-
);
|
|
31336
|
-
}
|
|
31337
|
-
}
|
|
31275
|
+
if (autoRegister) {
|
|
31276
|
+
await this.loadWidgetComponents(widgetName, widgetPath);
|
|
31338
31277
|
}
|
|
31339
31278
|
|
|
31340
|
-
return
|
|
31279
|
+
return config;
|
|
31341
31280
|
} catch (error) {
|
|
31342
|
-
console.error(
|
|
31343
|
-
|
|
31281
|
+
console.error(
|
|
31282
|
+
`[WidgetRegistry] Error downloading widget ${widgetName}:`,
|
|
31283
|
+
error,
|
|
31284
|
+
);
|
|
31285
|
+
throw error;
|
|
31344
31286
|
}
|
|
31345
|
-
}
|
|
31287
|
+
}
|
|
31346
31288
|
|
|
31347
|
-
|
|
31289
|
+
/**
|
|
31290
|
+
* Load widget configuration from local path
|
|
31291
|
+
* @param {string} widgetName - Name of the widget
|
|
31292
|
+
* @param {string} widgetPath - Path to widget directory
|
|
31293
|
+
* @returns {Promise<Object>} Widget configuration
|
|
31294
|
+
*/
|
|
31295
|
+
async loadWidgetConfig(widgetName, widgetPath) {
|
|
31348
31296
|
try {
|
|
31349
|
-
const
|
|
31350
|
-
|
|
31351
|
-
|
|
31297
|
+
const dashJsonPath = path.join(widgetPath, "dash.json");
|
|
31298
|
+
if (fs.existsSync(dashJsonPath)) {
|
|
31299
|
+
const data = fs.readFileSync(dashJsonPath, "utf8");
|
|
31300
|
+
return JSON.parse(data);
|
|
31301
|
+
}
|
|
31302
|
+
|
|
31303
|
+
const packageJsonPath = path.join(widgetPath, "package.json");
|
|
31304
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
31305
|
+
const packageJson = JSON.parse(
|
|
31306
|
+
fs.readFileSync(packageJsonPath, "utf8"),
|
|
31307
|
+
);
|
|
31352
31308
|
return {
|
|
31353
|
-
|
|
31354
|
-
|
|
31309
|
+
name: packageJson.name || widgetName,
|
|
31310
|
+
version: packageJson.version,
|
|
31311
|
+
description: packageJson.description,
|
|
31312
|
+
author: packageJson.author,
|
|
31313
|
+
repository: packageJson.repository,
|
|
31355
31314
|
};
|
|
31356
31315
|
}
|
|
31357
31316
|
|
|
31358
|
-
|
|
31317
|
+
return {
|
|
31318
|
+
name: widgetName,
|
|
31319
|
+
version: "1.0.0",
|
|
31320
|
+
};
|
|
31321
|
+
} catch (error) {
|
|
31322
|
+
console.error(
|
|
31323
|
+
`[WidgetRegistry] Error loading config for ${widgetName}:`,
|
|
31324
|
+
error,
|
|
31325
|
+
);
|
|
31326
|
+
return { name: widgetName };
|
|
31327
|
+
}
|
|
31328
|
+
}
|
|
31329
|
+
|
|
31330
|
+
/**
|
|
31331
|
+
* Register a widget in the registry
|
|
31332
|
+
* @param {string} widgetName - Name of the widget
|
|
31333
|
+
* @param {Object} config - Widget configuration
|
|
31334
|
+
* @param {string} widgetPath - Path to widget directory
|
|
31335
|
+
* @param {boolean} autoRegister - Automatically register with ComponentManager
|
|
31336
|
+
*/
|
|
31337
|
+
registerWidget(widgetName, config, widgetPath, autoRegister = true) {
|
|
31338
|
+
const widgetEntry = {
|
|
31339
|
+
name: widgetName,
|
|
31340
|
+
path: widgetPath,
|
|
31341
|
+
...config,
|
|
31342
|
+
registeredAt: new Date().toISOString(),
|
|
31343
|
+
};
|
|
31344
|
+
|
|
31345
|
+
this.widgets.set(widgetName, widgetEntry);
|
|
31346
|
+
this.saveRegistry();
|
|
31347
|
+
console.log(`[WidgetRegistry] Registered widget: ${widgetName}`);
|
|
31348
|
+
}
|
|
31359
31349
|
|
|
31360
|
-
|
|
31361
|
-
|
|
31350
|
+
/**
|
|
31351
|
+
* Load all components for a widget and register them with ComponentManager
|
|
31352
|
+
* @param {string} widgetName - Name of the widget
|
|
31353
|
+
* @param {string} widgetPath - Path to widget directory
|
|
31354
|
+
*/
|
|
31355
|
+
async loadWidgetComponents(widgetName, widgetPath) {
|
|
31356
|
+
try {
|
|
31357
|
+
// Auto-compile widget source to CJS bundle if none exists
|
|
31358
|
+
if (!findBundlePath(widgetPath)) {
|
|
31362
31359
|
try {
|
|
31363
|
-
|
|
31364
|
-
|
|
31365
|
-
bundlePath = compiled;
|
|
31366
|
-
}
|
|
31360
|
+
await compileWidget(widgetPath);
|
|
31361
|
+
console.log(`[WidgetRegistry] Auto-compiled ${widgetName}`);
|
|
31367
31362
|
} catch (compileError) {
|
|
31368
31363
|
console.warn(
|
|
31369
31364
|
`[WidgetRegistry] Could not compile ${widgetName}:`,
|
|
@@ -31372,430 +31367,435 @@ var schedulerController_1 = schedulerController$2;
|
|
|
31372
31367
|
}
|
|
31373
31368
|
}
|
|
31374
31369
|
|
|
31375
|
-
if (
|
|
31376
|
-
|
|
31377
|
-
success: false,
|
|
31378
|
-
error: `No bundle found in: ${widget.path}`,
|
|
31379
|
-
};
|
|
31370
|
+
if (this.componentManager) {
|
|
31371
|
+
dynamicWidgetLoader.setComponentManager(this.componentManager);
|
|
31380
31372
|
}
|
|
31381
31373
|
|
|
31382
|
-
const
|
|
31383
|
-
|
|
31384
|
-
|
|
31385
|
-
console.error(
|
|
31386
|
-
`[WidgetRegistry] Error reading bundle for ${widgetName}:`,
|
|
31387
|
-
error,
|
|
31374
|
+
const components = dynamicWidgetLoader.discoverWidgets(widgetPath);
|
|
31375
|
+
console.log(
|
|
31376
|
+
`[WidgetRegistry] Found ${components.length} components in ${widgetName}`,
|
|
31388
31377
|
);
|
|
31389
|
-
return { success: false, error: error.message };
|
|
31390
|
-
}
|
|
31391
|
-
});
|
|
31392
|
-
|
|
31393
|
-
ipcMain.handle("widget:read-all-bundles", async () => {
|
|
31394
|
-
try {
|
|
31395
|
-
const registry = getWidgetRegistry();
|
|
31396
|
-
const installedWidgets = registry.getWidgets();
|
|
31397
|
-
const results = [];
|
|
31398
|
-
|
|
31399
|
-
for (const widget of installedWidgets) {
|
|
31400
|
-
const widgetPath = widget.path;
|
|
31401
|
-
if (!widgetPath || !fs.existsSync(widgetPath)) continue;
|
|
31402
31378
|
|
|
31403
|
-
|
|
31379
|
+
const existingEntry = this.widgets.get(widgetName);
|
|
31380
|
+
let registryUpdated = false;
|
|
31404
31381
|
|
|
31405
|
-
|
|
31406
|
-
|
|
31407
|
-
|
|
31408
|
-
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
} catch (compileError) {
|
|
31413
|
-
console.warn(
|
|
31414
|
-
`[WidgetRegistry] Could not compile ${widget.name}:`,
|
|
31415
|
-
compileError,
|
|
31416
|
-
);
|
|
31417
|
-
}
|
|
31418
|
-
}
|
|
31382
|
+
// Store component names as displayName on the registry entry
|
|
31383
|
+
// so settings UI shows "WeatherWidget" instead of "weather-widget"
|
|
31384
|
+
if (components.length > 0 && existingEntry) {
|
|
31385
|
+
existingEntry.displayName = components.join(", ");
|
|
31386
|
+
existingEntry.componentNames = components;
|
|
31387
|
+
registryUpdated = true;
|
|
31388
|
+
}
|
|
31419
31389
|
|
|
31420
|
-
|
|
31421
|
-
|
|
31422
|
-
|
|
31390
|
+
for (const componentName of components) {
|
|
31391
|
+
try {
|
|
31392
|
+
const result = await dynamicWidgetLoader.loadWidget(
|
|
31393
|
+
widgetName,
|
|
31394
|
+
widgetPath,
|
|
31395
|
+
componentName,
|
|
31396
|
+
true,
|
|
31423
31397
|
);
|
|
31424
|
-
|
|
31425
|
-
}
|
|
31398
|
+
console.log(`[WidgetRegistry] ✓ Loaded ${componentName}`);
|
|
31426
31399
|
|
|
31427
|
-
|
|
31428
|
-
|
|
31429
|
-
|
|
31430
|
-
|
|
31431
|
-
|
|
31432
|
-
|
|
31433
|
-
|
|
31400
|
+
// Enrich registry entry with .dash.js config fields
|
|
31401
|
+
// (icon, providers, workspace, etc.) so the settings UI
|
|
31402
|
+
// has full display data without needing ComponentManager.
|
|
31403
|
+
if (result?.config && existingEntry) {
|
|
31404
|
+
const cfg = result.config;
|
|
31405
|
+
if (cfg.icon && !existingEntry.icon) existingEntry.icon = cfg.icon;
|
|
31406
|
+
if (cfg.providers?.length && !existingEntry.providers?.length)
|
|
31407
|
+
existingEntry.providers = cfg.providers;
|
|
31408
|
+
if (cfg.workspace && !existingEntry.workspace)
|
|
31409
|
+
existingEntry.workspace = cfg.workspace;
|
|
31410
|
+
if (cfg.events?.length && !existingEntry.events?.length)
|
|
31411
|
+
existingEntry.events = cfg.events;
|
|
31412
|
+
if (
|
|
31413
|
+
cfg.eventHandlers?.length &&
|
|
31414
|
+
!existingEntry.eventHandlers?.length
|
|
31415
|
+
)
|
|
31416
|
+
existingEntry.eventHandlers = cfg.eventHandlers;
|
|
31417
|
+
registryUpdated = true;
|
|
31418
|
+
}
|
|
31419
|
+
} catch (error) {
|
|
31434
31420
|
console.error(
|
|
31435
|
-
`[WidgetRegistry] Error
|
|
31436
|
-
|
|
31421
|
+
`[WidgetRegistry] Error loading component ${componentName}:`,
|
|
31422
|
+
error,
|
|
31437
31423
|
);
|
|
31438
31424
|
}
|
|
31439
31425
|
}
|
|
31440
31426
|
|
|
31441
|
-
|
|
31427
|
+
if (registryUpdated && existingEntry) {
|
|
31428
|
+
this.widgets.set(widgetName, existingEntry);
|
|
31429
|
+
this.saveRegistry();
|
|
31430
|
+
}
|
|
31442
31431
|
} catch (error) {
|
|
31443
|
-
console.error("[WidgetRegistry] Error
|
|
31444
|
-
return [];
|
|
31432
|
+
console.error("[WidgetRegistry] Error loading widget components:", error);
|
|
31445
31433
|
}
|
|
31446
|
-
}
|
|
31434
|
+
}
|
|
31447
31435
|
|
|
31448
|
-
|
|
31449
|
-
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
31453
|
-
|
|
31454
|
-
|
|
31455
|
-
return { success: false, error: error.message };
|
|
31456
|
-
}
|
|
31457
|
-
});
|
|
31458
|
-
}
|
|
31436
|
+
/**
|
|
31437
|
+
* Get all registered widgets
|
|
31438
|
+
* @returns {Array} List of widget configurations
|
|
31439
|
+
*/
|
|
31440
|
+
getWidgets() {
|
|
31441
|
+
return Array.from(this.widgets.values());
|
|
31442
|
+
}
|
|
31459
31443
|
|
|
31460
|
-
|
|
31461
|
-
|
|
31462
|
-
|
|
31463
|
-
|
|
31464
|
-
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
} (widgetRegistry$1));
|
|
31444
|
+
/**
|
|
31445
|
+
* Get widget by name
|
|
31446
|
+
* @param {string} widgetName - Name of the widget
|
|
31447
|
+
* @returns {Object|null} Widget configuration or null
|
|
31448
|
+
*/
|
|
31449
|
+
getWidget(widgetName) {
|
|
31450
|
+
return this.widgets.get(widgetName) || null;
|
|
31451
|
+
}
|
|
31469
31452
|
|
|
31470
|
-
|
|
31453
|
+
/**
|
|
31454
|
+
* Uninstall a widget
|
|
31455
|
+
* @param {string} widgetName - Name of the widget to remove
|
|
31456
|
+
*/
|
|
31457
|
+
uninstallWidget(widgetName) {
|
|
31458
|
+
const widget = this.widgets.get(widgetName);
|
|
31459
|
+
if (!widget) {
|
|
31460
|
+
console.warn(`[WidgetRegistry] Widget not found: ${widgetName}`);
|
|
31461
|
+
return false;
|
|
31462
|
+
}
|
|
31471
31463
|
|
|
31472
|
-
|
|
31473
|
-
|
|
31474
|
-
|
|
31475
|
-
|
|
31476
|
-
|
|
31477
|
-
|
|
31478
|
-
|
|
31479
|
-
|
|
31480
|
-
|
|
31481
|
-
|
|
31482
|
-
|
|
31483
|
-
|
|
31464
|
+
try {
|
|
31465
|
+
if (fs.existsSync(widget.path)) {
|
|
31466
|
+
fs.rmSync(widget.path, { recursive: true });
|
|
31467
|
+
}
|
|
31468
|
+
this.widgets.delete(widgetName);
|
|
31469
|
+
this.saveRegistry();
|
|
31470
|
+
console.log(`[WidgetRegistry] Uninstalled widget: ${widgetName}`);
|
|
31471
|
+
return true;
|
|
31472
|
+
} catch (error) {
|
|
31473
|
+
console.error(
|
|
31474
|
+
`[WidgetRegistry] Error uninstalling ${widgetName}:`,
|
|
31475
|
+
error,
|
|
31476
|
+
);
|
|
31477
|
+
return false;
|
|
31478
|
+
}
|
|
31479
|
+
}
|
|
31484
31480
|
|
|
31485
|
-
|
|
31486
|
-
|
|
31487
|
-
|
|
31481
|
+
/**
|
|
31482
|
+
* Helper: Fetch JSON from URL
|
|
31483
|
+
*/
|
|
31484
|
+
async fetchJSON(url) {
|
|
31485
|
+
const response = await fetch(url);
|
|
31486
|
+
if (!response.ok)
|
|
31487
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
31488
|
+
return response.json();
|
|
31489
|
+
}
|
|
31488
31490
|
|
|
31489
|
-
|
|
31490
|
-
|
|
31491
|
-
|
|
31492
|
-
|
|
31493
|
-
|
|
31494
|
-
|
|
31495
|
-
name: "dash-registry-auth",
|
|
31496
|
-
encryptionKey: "dash-registry-v1",
|
|
31497
|
-
});
|
|
31498
|
-
}
|
|
31499
|
-
return store$2;
|
|
31500
|
-
}
|
|
31491
|
+
/**
|
|
31492
|
+
* Get cache directory path
|
|
31493
|
+
*/
|
|
31494
|
+
getCachePath() {
|
|
31495
|
+
return WIDGETS_CACHE_DIR;
|
|
31496
|
+
}
|
|
31501
31497
|
|
|
31502
|
-
/**
|
|
31503
|
-
|
|
31504
|
-
|
|
31505
|
-
|
|
31506
|
-
|
|
31507
|
-
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
method: "POST",
|
|
31511
|
-
headers: { "Content-Type": "application/json" },
|
|
31512
|
-
});
|
|
31498
|
+
/**
|
|
31499
|
+
* Get the storage directory (parent of widgets directory)
|
|
31500
|
+
* @returns {string} Full path to storage directory
|
|
31501
|
+
*/
|
|
31502
|
+
getStoragePath() {
|
|
31503
|
+
return path.dirname(WIDGETS_CACHE_DIR);
|
|
31504
|
+
}
|
|
31505
|
+
}
|
|
31513
31506
|
|
|
31514
|
-
|
|
31515
|
-
|
|
31516
|
-
}
|
|
31507
|
+
// Lazy initialization to avoid accessing app.getPath before app is ready
|
|
31508
|
+
let widgetRegistry = null;
|
|
31517
31509
|
|
|
31518
|
-
|
|
31510
|
+
function getWidgetRegistry() {
|
|
31511
|
+
if (!widgetRegistry) {
|
|
31512
|
+
widgetRegistry = new WidgetRegistry();
|
|
31513
|
+
}
|
|
31514
|
+
return widgetRegistry;
|
|
31515
|
+
}
|
|
31519
31516
|
|
|
31520
|
-
|
|
31521
|
-
|
|
31522
|
-
|
|
31523
|
-
|
|
31524
|
-
|
|
31525
|
-
|
|
31526
|
-
|
|
31527
|
-
|
|
31528
|
-
|
|
31517
|
+
/**
|
|
31518
|
+
* Look for a CJS bundle file in a widget directory.
|
|
31519
|
+
* Checks multiple candidate paths in priority order because
|
|
31520
|
+
* packageZip.js may extract dist/ contents to the widget root.
|
|
31521
|
+
*
|
|
31522
|
+
* @param {string} widgetPath - Path to the widget directory
|
|
31523
|
+
* @returns {string|null} Absolute path to the bundle, or null if not found
|
|
31524
|
+
*/
|
|
31525
|
+
function findBundlePath(widgetPath) {
|
|
31526
|
+
const candidates = [
|
|
31527
|
+
path.join(widgetPath, "dist", "index.cjs.js"),
|
|
31528
|
+
path.join(widgetPath, "index.cjs.js"),
|
|
31529
|
+
path.join(widgetPath, "dist", "index.js"),
|
|
31530
|
+
path.join(widgetPath, "index.js"),
|
|
31531
|
+
];
|
|
31529
31532
|
|
|
31530
|
-
|
|
31531
|
-
|
|
31532
|
-
|
|
31533
|
-
|
|
31534
|
-
|
|
31535
|
-
|
|
31536
|
-
|
|
31537
|
-
|
|
31538
|
-
|
|
31539
|
-
|
|
31533
|
+
for (const candidate of candidates) {
|
|
31534
|
+
if (fs.existsSync(candidate)) {
|
|
31535
|
+
// Skip ESM files — the eval pipeline requires CJS
|
|
31536
|
+
if (candidate.endsWith(".js") && !candidate.endsWith(".cjs.js")) {
|
|
31537
|
+
try {
|
|
31538
|
+
const head = fs.readFileSync(candidate, "utf8").slice(0, 256);
|
|
31539
|
+
if (/^\s*(import\s|export\s)/m.test(head)) {
|
|
31540
|
+
console.log(`[WidgetRegistry] Skipping ESM bundle: ${candidate}`);
|
|
31541
|
+
continue;
|
|
31542
|
+
}
|
|
31543
|
+
} catch (_) {
|
|
31544
|
+
// Non-fatal — allow fallthrough
|
|
31545
|
+
}
|
|
31546
|
+
}
|
|
31547
|
+
return candidate;
|
|
31548
|
+
}
|
|
31549
|
+
}
|
|
31540
31550
|
|
|
31541
|
-
|
|
31542
|
-
|
|
31543
|
-
}
|
|
31551
|
+
return null;
|
|
31552
|
+
}
|
|
31544
31553
|
|
|
31545
|
-
|
|
31546
|
-
|
|
31547
|
-
|
|
31548
|
-
|
|
31549
|
-
|
|
31550
|
-
return { status: "pending" };
|
|
31551
|
-
}
|
|
31554
|
+
/**
|
|
31555
|
+
* Setup IPC handlers for widget management (use in main.js)
|
|
31556
|
+
*/
|
|
31557
|
+
function setupWidgetRegistryHandlers() {
|
|
31558
|
+
ipcMain.handle("widget:list", () => getWidgetRegistry().getWidgets());
|
|
31552
31559
|
|
|
31553
|
-
|
|
31554
|
-
|
|
31560
|
+
ipcMain.handle("widget:get", (event, widgetName) => {
|
|
31561
|
+
return getWidgetRegistry().getWidget(widgetName);
|
|
31562
|
+
});
|
|
31555
31563
|
|
|
31556
|
-
|
|
31557
|
-
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31564
|
+
ipcMain.handle(
|
|
31565
|
+
"widget:install",
|
|
31566
|
+
async (event, widgetName, downloadUrl, dashConfigUrl) => {
|
|
31567
|
+
const config = await getWidgetRegistry().downloadWidget(
|
|
31568
|
+
widgetName,
|
|
31569
|
+
downloadUrl,
|
|
31570
|
+
dashConfigUrl,
|
|
31571
|
+
);
|
|
31562
31572
|
|
|
31563
|
-
|
|
31564
|
-
|
|
31565
|
-
|
|
31566
|
-
|
|
31567
|
-
|
|
31568
|
-
|
|
31573
|
+
BrowserWindow.getAllWindows().forEach((win) => {
|
|
31574
|
+
win.webContents.send("widget:installed", {
|
|
31575
|
+
widgetName,
|
|
31576
|
+
config,
|
|
31577
|
+
});
|
|
31578
|
+
});
|
|
31569
31579
|
|
|
31570
|
-
|
|
31571
|
-
}
|
|
31580
|
+
return config;
|
|
31581
|
+
},
|
|
31582
|
+
);
|
|
31572
31583
|
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
|
|
31580
|
-
|
|
31581
|
-
|
|
31582
|
-
if (!token) return null;
|
|
31584
|
+
ipcMain.handle(
|
|
31585
|
+
"widget:install-local",
|
|
31586
|
+
async (event, widgetName, localPath, dashConfigPath) => {
|
|
31587
|
+
const config = await getWidgetRegistry().installFromLocalPath(
|
|
31588
|
+
widgetName,
|
|
31589
|
+
localPath,
|
|
31590
|
+
true,
|
|
31591
|
+
dashConfigPath,
|
|
31592
|
+
);
|
|
31583
31593
|
|
|
31584
|
-
|
|
31585
|
-
|
|
31586
|
-
|
|
31587
|
-
|
|
31588
|
-
|
|
31589
|
-
|
|
31590
|
-
return null;
|
|
31591
|
-
}
|
|
31592
|
-
}
|
|
31594
|
+
BrowserWindow.getAllWindows().forEach((win) => {
|
|
31595
|
+
win.webContents.send("widget:installed", {
|
|
31596
|
+
widgetName,
|
|
31597
|
+
config,
|
|
31598
|
+
});
|
|
31599
|
+
});
|
|
31593
31600
|
|
|
31594
|
-
|
|
31595
|
-
|
|
31596
|
-
|
|
31597
|
-
* @returns {Object} { authenticated: boolean, userId?: string }
|
|
31598
|
-
*/
|
|
31599
|
-
function getAuthStatus$1() {
|
|
31600
|
-
const stored = getStoredToken$2();
|
|
31601
|
-
if (!stored) {
|
|
31602
|
-
return { authenticated: false };
|
|
31603
|
-
}
|
|
31601
|
+
return config;
|
|
31602
|
+
},
|
|
31603
|
+
);
|
|
31604
31604
|
|
|
31605
|
-
|
|
31606
|
-
|
|
31607
|
-
|
|
31608
|
-
|
|
31609
|
-
|
|
31610
|
-
}
|
|
31605
|
+
ipcMain.handle("widget:load-folder", async (event, folderPath) => {
|
|
31606
|
+
const results = await getWidgetRegistry().registerWidgetsFromFolder(
|
|
31607
|
+
folderPath,
|
|
31608
|
+
true,
|
|
31609
|
+
);
|
|
31611
31610
|
|
|
31612
|
-
|
|
31613
|
-
|
|
31614
|
-
|
|
31615
|
-
|
|
31616
|
-
|
|
31617
|
-
|
|
31618
|
-
const stored = getStoredToken$2();
|
|
31619
|
-
if (!stored) return null;
|
|
31611
|
+
BrowserWindow.getAllWindows().forEach((win) => {
|
|
31612
|
+
win.webContents.send("widgets:loaded", {
|
|
31613
|
+
count: results.length,
|
|
31614
|
+
widgets: results,
|
|
31615
|
+
});
|
|
31616
|
+
});
|
|
31620
31617
|
|
|
31621
|
-
|
|
31622
|
-
|
|
31623
|
-
headers: {
|
|
31624
|
-
Authorization: `Bearer ${stored.token}`,
|
|
31625
|
-
},
|
|
31626
|
-
});
|
|
31618
|
+
return results;
|
|
31619
|
+
});
|
|
31627
31620
|
|
|
31628
|
-
|
|
31629
|
-
|
|
31630
|
-
|
|
31631
|
-
|
|
31632
|
-
|
|
31633
|
-
if (!response.ok) return null;
|
|
31621
|
+
ipcMain.handle("widget:uninstall", (event, widgetName) => {
|
|
31622
|
+
const schedulerController = schedulerController_1;
|
|
31623
|
+
schedulerController.cleanupWidget(widgetName);
|
|
31624
|
+
return getWidgetRegistry().uninstallWidget(widgetName);
|
|
31625
|
+
});
|
|
31634
31626
|
|
|
31635
|
-
|
|
31636
|
-
return data.user || null;
|
|
31637
|
-
} catch {
|
|
31638
|
-
return null;
|
|
31639
|
-
}
|
|
31640
|
-
}
|
|
31627
|
+
ipcMain.handle("widget:cache-path", () => getWidgetRegistry().getCachePath());
|
|
31641
31628
|
|
|
31642
|
-
|
|
31643
|
-
|
|
31644
|
-
|
|
31645
|
-
function clearToken() {
|
|
31646
|
-
try {
|
|
31647
|
-
const s = getStore$1();
|
|
31648
|
-
s.clear();
|
|
31649
|
-
console.log("[RegistryAuthController] Token cleared");
|
|
31650
|
-
} catch (err) {
|
|
31651
|
-
console.error("[RegistryAuthController] Error clearing token:", err);
|
|
31652
|
-
}
|
|
31653
|
-
}
|
|
31629
|
+
ipcMain.handle("widget:storage-path", () =>
|
|
31630
|
+
getWidgetRegistry().getStoragePath(),
|
|
31631
|
+
);
|
|
31654
31632
|
|
|
31655
|
-
|
|
31656
|
-
|
|
31657
|
-
|
|
31658
|
-
|
|
31659
|
-
|
|
31660
|
-
|
|
31661
|
-
|
|
31662
|
-
|
|
31663
|
-
|
|
31633
|
+
ipcMain.handle("widget:get-component-configs", async () => {
|
|
31634
|
+
try {
|
|
31635
|
+
const registry = getWidgetRegistry();
|
|
31636
|
+
const installedWidgets = registry.getWidgets();
|
|
31637
|
+
const configs = [];
|
|
31638
|
+
|
|
31639
|
+
for (const widget of installedWidgets) {
|
|
31640
|
+
const widgetPath = widget.path;
|
|
31641
|
+
if (!widgetPath || !fs.existsSync(widgetPath)) continue;
|
|
31642
|
+
|
|
31643
|
+
const componentNames = dynamicWidgetLoader.discoverWidgets(widgetPath);
|
|
31644
|
+
const widgetsDir = findWidgetsDir(widgetPath);
|
|
31645
|
+
for (const componentName of componentNames) {
|
|
31646
|
+
try {
|
|
31647
|
+
const configPath = path.join(
|
|
31648
|
+
widgetsDir || path.join(widgetPath, "widgets"),
|
|
31649
|
+
`${componentName}.dash.js`,
|
|
31650
|
+
);
|
|
31651
|
+
const config = await dynamicWidgetLoader.loadConfigFile(configPath);
|
|
31652
|
+
configs.push({
|
|
31653
|
+
componentName,
|
|
31654
|
+
widgetPackage: widget.name,
|
|
31655
|
+
// Include scoped id if present in the config
|
|
31656
|
+
id: config.id || null,
|
|
31657
|
+
config,
|
|
31658
|
+
});
|
|
31659
|
+
} catch (err) {
|
|
31660
|
+
console.error(
|
|
31661
|
+
`[WidgetRegistry] Error loading config for ${componentName}:`,
|
|
31662
|
+
err,
|
|
31663
|
+
);
|
|
31664
|
+
}
|
|
31665
|
+
}
|
|
31666
|
+
}
|
|
31664
31667
|
|
|
31665
|
-
|
|
31666
|
-
|
|
31667
|
-
|
|
31668
|
-
|
|
31669
|
-
|
|
31670
|
-
|
|
31671
|
-
},
|
|
31672
|
-
body: JSON.stringify(updates),
|
|
31673
|
-
});
|
|
31668
|
+
return configs;
|
|
31669
|
+
} catch (error) {
|
|
31670
|
+
console.error("[WidgetRegistry] Error getting component configs:", error);
|
|
31671
|
+
return [];
|
|
31672
|
+
}
|
|
31673
|
+
});
|
|
31674
31674
|
|
|
31675
|
-
|
|
31676
|
-
|
|
31677
|
-
|
|
31678
|
-
|
|
31679
|
-
|
|
31675
|
+
ipcMain.handle("widget:read-bundle", async (event, widgetName) => {
|
|
31676
|
+
try {
|
|
31677
|
+
const registry = getWidgetRegistry();
|
|
31678
|
+
const widget = registry.getWidget(widgetName);
|
|
31679
|
+
if (!widget || !widget.path) {
|
|
31680
|
+
return {
|
|
31681
|
+
success: false,
|
|
31682
|
+
error: `Widget not found: ${widgetName}`,
|
|
31683
|
+
};
|
|
31684
|
+
}
|
|
31680
31685
|
|
|
31681
|
-
|
|
31682
|
-
return data.user || null;
|
|
31683
|
-
} catch {
|
|
31684
|
-
return null;
|
|
31685
|
-
}
|
|
31686
|
-
}
|
|
31686
|
+
let bundlePath = findBundlePath(widget.path);
|
|
31687
31687
|
|
|
31688
|
-
|
|
31689
|
-
|
|
31690
|
-
|
|
31691
|
-
|
|
31692
|
-
|
|
31693
|
-
|
|
31694
|
-
|
|
31695
|
-
|
|
31688
|
+
// Auto-compile if no bundle exists (same as read-all-bundles)
|
|
31689
|
+
if (!bundlePath) {
|
|
31690
|
+
try {
|
|
31691
|
+
const compiled = await compileWidget(widget.path);
|
|
31692
|
+
if (compiled) {
|
|
31693
|
+
bundlePath = compiled;
|
|
31694
|
+
}
|
|
31695
|
+
} catch (compileError) {
|
|
31696
|
+
console.warn(
|
|
31697
|
+
`[WidgetRegistry] Could not compile ${widgetName}:`,
|
|
31698
|
+
compileError,
|
|
31699
|
+
);
|
|
31700
|
+
}
|
|
31701
|
+
}
|
|
31696
31702
|
|
|
31697
|
-
|
|
31698
|
-
|
|
31699
|
-
|
|
31700
|
-
|
|
31701
|
-
|
|
31702
|
-
|
|
31703
|
+
if (!bundlePath) {
|
|
31704
|
+
return {
|
|
31705
|
+
success: false,
|
|
31706
|
+
error: `No bundle found in: ${widget.path}`,
|
|
31707
|
+
};
|
|
31708
|
+
}
|
|
31703
31709
|
|
|
31704
|
-
|
|
31705
|
-
|
|
31706
|
-
|
|
31707
|
-
|
|
31708
|
-
|
|
31710
|
+
const source = fs.readFileSync(bundlePath, "utf8");
|
|
31711
|
+
return { success: true, source, widgetName };
|
|
31712
|
+
} catch (error) {
|
|
31713
|
+
console.error(
|
|
31714
|
+
`[WidgetRegistry] Error reading bundle for ${widgetName}:`,
|
|
31715
|
+
error,
|
|
31716
|
+
);
|
|
31717
|
+
return { success: false, error: error.message };
|
|
31718
|
+
}
|
|
31719
|
+
});
|
|
31709
31720
|
|
|
31710
|
-
|
|
31711
|
-
|
|
31712
|
-
|
|
31713
|
-
|
|
31714
|
-
|
|
31721
|
+
ipcMain.handle("widget:read-all-bundles", async () => {
|
|
31722
|
+
try {
|
|
31723
|
+
const registry = getWidgetRegistry();
|
|
31724
|
+
const installedWidgets = registry.getWidgets();
|
|
31725
|
+
const results = [];
|
|
31715
31726
|
|
|
31716
|
-
|
|
31717
|
-
|
|
31718
|
-
|
|
31719
|
-
* @param {string} scope - Package scope (e.g. "@trops")
|
|
31720
|
-
* @param {string} name - Package name
|
|
31721
|
-
* @param {Object} updates - Fields to update (displayName, description, category, tags, visibility)
|
|
31722
|
-
* @returns {Promise<Object|null>} Updated package or null
|
|
31723
|
-
*/
|
|
31724
|
-
async function updateRegistryPackage$1(scope, name, updates) {
|
|
31725
|
-
const stored = getStoredToken$2();
|
|
31726
|
-
if (!stored) return null;
|
|
31727
|
+
for (const widget of installedWidgets) {
|
|
31728
|
+
const widgetPath = widget.path;
|
|
31729
|
+
if (!widgetPath || !fs.existsSync(widgetPath)) continue;
|
|
31727
31730
|
|
|
31728
|
-
|
|
31729
|
-
const response = await fetch(
|
|
31730
|
-
`${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
|
|
31731
|
-
{
|
|
31732
|
-
method: "PATCH",
|
|
31733
|
-
headers: {
|
|
31734
|
-
Authorization: `Bearer ${stored.token}`,
|
|
31735
|
-
"Content-Type": "application/json",
|
|
31736
|
-
},
|
|
31737
|
-
body: JSON.stringify(updates),
|
|
31738
|
-
},
|
|
31739
|
-
);
|
|
31731
|
+
let bundlePath = findBundlePath(widgetPath);
|
|
31740
31732
|
|
|
31741
|
-
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
|
|
31733
|
+
// Auto-compile if no bundle exists
|
|
31734
|
+
if (!bundlePath) {
|
|
31735
|
+
try {
|
|
31736
|
+
const compiled = await compileWidget(widgetPath);
|
|
31737
|
+
if (compiled) {
|
|
31738
|
+
bundlePath = compiled;
|
|
31739
|
+
}
|
|
31740
|
+
} catch (compileError) {
|
|
31741
|
+
console.warn(
|
|
31742
|
+
`[WidgetRegistry] Could not compile ${widget.name}:`,
|
|
31743
|
+
compileError,
|
|
31744
|
+
);
|
|
31745
|
+
}
|
|
31746
|
+
}
|
|
31746
31747
|
|
|
31747
|
-
|
|
31748
|
-
|
|
31749
|
-
|
|
31750
|
-
|
|
31751
|
-
|
|
31748
|
+
if (!bundlePath) {
|
|
31749
|
+
console.log(
|
|
31750
|
+
`[WidgetRegistry] No CJS bundle for ${widget.name}, skipping (will use config fallback)`,
|
|
31751
|
+
);
|
|
31752
|
+
continue;
|
|
31753
|
+
}
|
|
31752
31754
|
|
|
31753
|
-
|
|
31754
|
-
|
|
31755
|
-
|
|
31756
|
-
|
|
31757
|
-
|
|
31758
|
-
|
|
31759
|
-
|
|
31760
|
-
|
|
31761
|
-
|
|
31762
|
-
|
|
31755
|
+
try {
|
|
31756
|
+
const source = fs.readFileSync(bundlePath, "utf8");
|
|
31757
|
+
results.push({
|
|
31758
|
+
widgetName: widget.name,
|
|
31759
|
+
source,
|
|
31760
|
+
});
|
|
31761
|
+
} catch (readError) {
|
|
31762
|
+
console.error(
|
|
31763
|
+
`[WidgetRegistry] Error reading bundle for ${widget.name}:`,
|
|
31764
|
+
readError,
|
|
31765
|
+
);
|
|
31766
|
+
}
|
|
31767
|
+
}
|
|
31763
31768
|
|
|
31764
|
-
|
|
31765
|
-
|
|
31766
|
-
|
|
31767
|
-
|
|
31768
|
-
|
|
31769
|
-
|
|
31770
|
-
Authorization: `Bearer ${stored.token}`,
|
|
31771
|
-
},
|
|
31772
|
-
},
|
|
31773
|
-
);
|
|
31769
|
+
return results;
|
|
31770
|
+
} catch (error) {
|
|
31771
|
+
console.error("[WidgetRegistry] Error reading all bundles:", error);
|
|
31772
|
+
return [];
|
|
31773
|
+
}
|
|
31774
|
+
});
|
|
31774
31775
|
|
|
31775
|
-
|
|
31776
|
-
|
|
31777
|
-
|
|
31778
|
-
|
|
31779
|
-
|
|
31776
|
+
ipcMain.handle("widget:set-storage-path", (event, customPath) => {
|
|
31777
|
+
try {
|
|
31778
|
+
WidgetRegistry.initialize(customPath);
|
|
31779
|
+
console.log(`[WidgetRegistry] Storage path changed to: ${customPath}`);
|
|
31780
|
+
return { success: true, path: customPath };
|
|
31781
|
+
} catch (error) {
|
|
31782
|
+
console.error("[WidgetRegistry] Error setting storage path:", error);
|
|
31783
|
+
return { success: false, error: error.message };
|
|
31784
|
+
}
|
|
31785
|
+
});
|
|
31786
|
+
}
|
|
31780
31787
|
|
|
31781
|
-
|
|
31782
|
-
|
|
31783
|
-
|
|
31784
|
-
|
|
31785
|
-
|
|
31788
|
+
module.exports = WidgetRegistry;
|
|
31789
|
+
module.exports.getWidgetRegistry = getWidgetRegistry;
|
|
31790
|
+
// For backward compatibility, provide widgetRegistry as a getter
|
|
31791
|
+
Object.defineProperty(module.exports, "widgetRegistry", {
|
|
31792
|
+
get: getWidgetRegistry,
|
|
31793
|
+
});
|
|
31794
|
+
module.exports.setupWidgetRegistryHandlers = setupWidgetRegistryHandlers;
|
|
31795
|
+
module.exports.validateZipEntries = validateZipEntries;
|
|
31796
|
+
} (widgetRegistry$1));
|
|
31786
31797
|
|
|
31787
|
-
var
|
|
31788
|
-
initiateDeviceFlow: initiateDeviceFlow$1,
|
|
31789
|
-
pollForToken: pollForToken$1,
|
|
31790
|
-
getStoredToken: getStoredToken$2,
|
|
31791
|
-
getAuthStatus: getAuthStatus$1,
|
|
31792
|
-
getRegistryProfile: getRegistryProfile$2,
|
|
31793
|
-
updateRegistryProfile: updateRegistryProfile$1,
|
|
31794
|
-
getRegistryPackages: getRegistryPackages$1,
|
|
31795
|
-
updateRegistryPackage: updateRegistryPackage$1,
|
|
31796
|
-
deleteRegistryPackage,
|
|
31797
|
-
clearToken,
|
|
31798
|
-
};
|
|
31798
|
+
var widgetRegistryExports = widgetRegistry$1.exports;
|
|
31799
31799
|
|
|
31800
31800
|
/**
|
|
31801
31801
|
* registryApiController.js
|
|
@@ -31806,7 +31806,7 @@ var registryAuthController$1 = {
|
|
|
31806
31806
|
|
|
31807
31807
|
const fs = require$$0$3;
|
|
31808
31808
|
const path$3 = require$$1$2;
|
|
31809
|
-
const { getStoredToken: getStoredToken$
|
|
31809
|
+
const { getStoredToken: getStoredToken$2 } = registryAuthController$1;
|
|
31810
31810
|
|
|
31811
31811
|
const REGISTRY_BASE_URL =
|
|
31812
31812
|
process.env.DASH_REGISTRY_API_URL ||
|
|
@@ -31820,7 +31820,7 @@ const REGISTRY_BASE_URL =
|
|
|
31820
31820
|
* @returns {Promise<Object>} { success, registryUrl, packageId, version, error? }
|
|
31821
31821
|
*/
|
|
31822
31822
|
async function publishToRegistry$1(zipPath, manifest) {
|
|
31823
|
-
const auth = getStoredToken$
|
|
31823
|
+
const auth = getStoredToken$2();
|
|
31824
31824
|
if (!auth) {
|
|
31825
31825
|
return {
|
|
31826
31826
|
success: false,
|
|
@@ -31912,7 +31912,7 @@ const registryApiController$1 = registryApiController$2;
|
|
|
31912
31912
|
const {
|
|
31913
31913
|
getAuthStatus,
|
|
31914
31914
|
getRegistryProfile: getRegistryProfile$1,
|
|
31915
|
-
getStoredToken,
|
|
31915
|
+
getStoredToken: getStoredToken$1,
|
|
31916
31916
|
} = registryAuthController$1;
|
|
31917
31917
|
|
|
31918
31918
|
/**
|
|
@@ -32159,7 +32159,7 @@ async function installThemeFromRegistry$1(win, appId, packageName) {
|
|
|
32159
32159
|
|
|
32160
32160
|
// Download the ZIP (with auth header)
|
|
32161
32161
|
const headers = {};
|
|
32162
|
-
const auth = getStoredToken();
|
|
32162
|
+
const auth = getStoredToken$1();
|
|
32163
32163
|
if (auth?.token) {
|
|
32164
32164
|
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
32165
32165
|
}
|
|
@@ -32324,6 +32324,7 @@ const {
|
|
|
32324
32324
|
applyEventWiringToLayout,
|
|
32325
32325
|
} = dashboardConfigUtils$1;
|
|
32326
32326
|
const { searchRegistry, getPackage } = registryController$3;
|
|
32327
|
+
const { getStoredToken } = registryAuthController$1;
|
|
32327
32328
|
const themeController$3 = themeController_1;
|
|
32328
32329
|
|
|
32329
32330
|
const configFilename = "workspaces.json";
|
|
@@ -32947,7 +32948,13 @@ async function installDashboardFromRegistry$1(
|
|
|
32947
32948
|
`[DashboardConfigController] Fetching dashboard from: ${downloadUrl}`,
|
|
32948
32949
|
);
|
|
32949
32950
|
|
|
32950
|
-
|
|
32951
|
+
// Download the ZIP (with auth header)
|
|
32952
|
+
const headers = {};
|
|
32953
|
+
const auth = getStoredToken();
|
|
32954
|
+
if (auth?.token) {
|
|
32955
|
+
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
32956
|
+
}
|
|
32957
|
+
const response = await fetch(downloadUrl, { headers });
|
|
32951
32958
|
if (!response.ok) {
|
|
32952
32959
|
return {
|
|
32953
32960
|
success: false,
|