@trops/dash-core 0.1.415 → 0.1.417

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.
@@ -63840,7 +63840,13 @@ function generateRegistryManifest(dashboardConfig, options = {}) {
63840
63840
  .toLowerCase();
63841
63841
 
63842
63842
  const githubUser = options.githubUser || "";
63843
- const version = "1.0.0";
63843
+ // Prefer an explicitly-passed version (caller resolved it already,
63844
+ // e.g. via resolveNextVersion + bump). Fall back to the dashboard's
63845
+ // own version, then a 1.0.0 baseline. The previous hardcoded
63846
+ // "1.0.0" meant every republish looked identical to the registry;
63847
+ // latestVersion never advanced, so downstream update-check never
63848
+ // saw a diff and no notification fired.
63849
+ const version = options.version || dashboardConfig.version || "1.0.0";
63844
63850
  const visibility = options.visibility === "private" ? "private" : "public";
63845
63851
 
63846
63852
  const manifest = {
@@ -63867,13 +63873,31 @@ function generateRegistryManifest(dashboardConfig, options = {}) {
63867
63873
  options.callerScope && w.scope && w.scope !== options.callerScope
63868
63874
  ? options.callerScope
63869
63875
  : w.scope || "";
63876
+ // Packaged id — the scoped "@<scope>/<packageName>" string that
63877
+ // the install flow looks up in the registry. Build this from the
63878
+ // REMAPPED scope + bare packageName so installers resolve against
63879
+ // the scope the widget was actually published as, not the local
63880
+ // `@ai-built` convention. Stripping the scope prefix from a
63881
+ // potentially-scoped packageName keeps the result canonical.
63882
+ const bareName = stripScopePrefix(
63883
+ w.packageName || w.package || "",
63884
+ remappedScope || w.scope,
63885
+ );
63886
+ const scopedPackageId = remappedScope
63887
+ ? `@${remappedScope.replace(/^@/, "")}/${bareName}`
63888
+ : bareName;
63870
63889
  return {
63871
63890
  id: w.id,
63872
63891
  scope: remappedScope,
63873
- packageName: w.packageName || w.package || "",
63892
+ packageName: bareName,
63874
63893
  widgetName: w.widgetName || (w.id ? w.id.split(".").pop() : w.package),
63875
63894
  name: w.id ? w.id.split(".").pop() : w.package,
63876
- package: w.package,
63895
+ // `package` is consumed by the install flow as the registry
63896
+ // package id (see installDashboardFromRegistry in
63897
+ // dashboardConfigController.js). Must carry the remapped
63898
+ // scope, otherwise installers look up an @ai-built/... id that
63899
+ // only exists on the publisher's machine.
63900
+ package: scopedPackageId,
63877
63901
  version: w.version || "*",
63878
63902
  required: w.required !== false,
63879
63903
  author: w.author || "",
@@ -63982,11 +64006,27 @@ function buildDashboardPreview(source) {
63982
64006
  * @returns {Array} Update records with workspace info and version comparison
63983
64007
  */
63984
64008
  function checkDashboardUpdates(workspaces = [], registryPackages = []) {
63985
- const registryByName = new Map();
64009
+ // Index the registry by canonical `@scope/name` so the lookup
64010
+ // survives installs that capture scope differently from the
64011
+ // registry's raw `name` field. The previous map was keyed by bare
64012
+ // `pkg.name`, which misses anytime the installed config recorded
64013
+ // `@scope/name` (our new publish flow writes that) or when two
64014
+ // users publish the same bare name. Key both forms for safety.
64015
+ const registryByKey = new Map();
64016
+ const asKey = (scope, name) => {
64017
+ if (!name) return null;
64018
+ if (!scope) return name;
64019
+ const bareScope = String(scope).replace(/^@/, "");
64020
+ return `@${bareScope}/${name}`;
64021
+ };
63986
64022
  for (const pkg of registryPackages) {
63987
- if (pkg.name && (pkg.type || "widget") === "dashboard") {
63988
- registryByName.set(pkg.name, pkg);
63989
- }
64023
+ if (!pkg.name) continue;
64024
+ if ((pkg.type || "widget") !== "dashboard") continue;
64025
+ const scoped = asKey(pkg.scope, pkg.name);
64026
+ if (scoped) registryByKey.set(scoped, pkg);
64027
+ // Back-compat: also store under bare name so installed configs
64028
+ // that predate the scope-aware write continue to match.
64029
+ registryByKey.set(pkg.name, pkg);
63990
64030
  }
63991
64031
 
63992
64032
  const updates = [];
@@ -63995,7 +64035,24 @@ function checkDashboardUpdates(workspaces = [], registryPackages = []) {
63995
64035
  const config = ws._dashboardConfig;
63996
64036
  if (!config || !config.registryPackage) continue;
63997
64037
 
63998
- const registryPkg = registryByName.get(config.registryPackage);
64038
+ // Lookup chain: try the scoped form first, fall back to bare. The
64039
+ // installed config may record either. Scope is stored on the
64040
+ // config by the install flow (or set here by the publish persist
64041
+ // step we just added).
64042
+ const installedScope =
64043
+ config.registryScope ||
64044
+ (config.registryPackage.startsWith("@")
64045
+ ? config.registryPackage.slice(1).split("/")[0]
64046
+ : null);
64047
+ const installedName = config.registryPackage.includes("/")
64048
+ ? config.registryPackage.split("/").pop()
64049
+ : config.registryPackage;
64050
+ const scopedKey = asKey(installedScope, installedName);
64051
+
64052
+ const registryPkg =
64053
+ (scopedKey && registryByKey.get(scopedKey)) ||
64054
+ registryByKey.get(config.registryPackage) ||
64055
+ registryByKey.get(installedName);
63999
64056
  if (!registryPkg) continue;
64000
64057
 
64001
64058
  const installedVersion = config.installedVersion || "0.0.0";
@@ -64006,6 +64063,7 @@ function checkDashboardUpdates(workspaces = [], registryPackages = []) {
64006
64063
  workspaceId: ws.id,
64007
64064
  workspaceName: ws.name || ws.label || "",
64008
64065
  registryPackage: config.registryPackage,
64066
+ registryScope: installedScope,
64009
64067
  installedVersion,
64010
64068
  latestVersion,
64011
64069
  importedAt: config.importedAt || null,
@@ -64151,6 +64209,67 @@ function extractEventWiringFromWorkspace$1(workspace) {
64151
64209
  return wiring;
64152
64210
  }
64153
64211
 
64212
+ /**
64213
+ * Strip publisher-specific personalization (userPrefs + selectedProviders)
64214
+ * from every widget instance in a layout-ish structure. Used by the
64215
+ * dashboard publish flow so the installer starts with the widget's
64216
+ * own defaultValue on every field instead of inheriting the
64217
+ * publisher's absolute paths, region tags, credentials, etc.
64218
+ *
64219
+ * Walks the standard layout shapes that forEachWidget handles:
64220
+ * - top-level `layout` arrays
64221
+ * - `workspace.pages[*].layout`
64222
+ * - `workspace.sidebarLayout`
64223
+ * - `LayoutGridContainer` children stored on `item.items` / `item.layout`
64224
+ *
64225
+ * Returns a deep copy — never mutates the input workspace.
64226
+ *
64227
+ * Title-ish defaults (widget.name) are intentionally preserved — they
64228
+ * are part of the dashboard template, not personal data. Anything else
64229
+ * under userPrefs is dropped; the installer's widget re-reads the
64230
+ * `defaultValue` declared in the component's `.dash.js`.
64231
+ */
64232
+ function stripPersonalizationFromWorkspace$1(workspace) {
64233
+ if (!workspace) return workspace;
64234
+ const cleanItem = (item) => {
64235
+ if (!item || typeof item !== "object") return item;
64236
+ // Preserve the layout position + children, but blank out the
64237
+ // user-set config values that are tied to the publisher's machine.
64238
+ const cleaned = { ...item };
64239
+ if ("userPrefs" in cleaned) delete cleaned.userPrefs;
64240
+ if ("selectedProviders" in cleaned) delete cleaned.selectedProviders;
64241
+ if (Array.isArray(cleaned.items)) {
64242
+ cleaned.items = cleaned.items.map(cleanItem);
64243
+ }
64244
+ if (Array.isArray(cleaned.layout)) {
64245
+ cleaned.layout = cleaned.layout.map(cleanItem);
64246
+ }
64247
+ return cleaned;
64248
+ };
64249
+ const cleaned = { ...workspace };
64250
+ if (Array.isArray(cleaned.layout))
64251
+ cleaned.layout = cleaned.layout.map(cleanItem);
64252
+ if (Array.isArray(cleaned.sidebarLayout))
64253
+ cleaned.sidebarLayout = cleaned.sidebarLayout.map(cleanItem);
64254
+ if (Array.isArray(cleaned.pages)) {
64255
+ cleaned.pages = cleaned.pages.map((page) =>
64256
+ page
64257
+ ? {
64258
+ ...page,
64259
+ ...(Array.isArray(page.layout)
64260
+ ? { layout: page.layout.map(cleanItem) }
64261
+ : {}),
64262
+ }
64263
+ : page,
64264
+ );
64265
+ }
64266
+ // Workspace-level selectedProviders map lives at the top level for
64267
+ // some older workspaces; drop it too so the installer doesn't get
64268
+ // bindings to provider names that don't exist on their machine.
64269
+ if ("selectedProviders" in cleaned) delete cleaned.selectedProviders;
64270
+ return cleaned;
64271
+ }
64272
+
64154
64273
  var dashboardConfigUtils$1 = {
64155
64274
  collectComponentNames: collectComponentNames$1,
64156
64275
  collectComponentNamesFromWorkspace: collectComponentNamesFromWorkspace$1,
@@ -64165,6 +64284,7 @@ var dashboardConfigUtils$1 = {
64165
64284
  checkDashboardUpdates,
64166
64285
  buildProviderSetupManifest,
64167
64286
  checkApiCompatibility,
64287
+ stripPersonalizationFromWorkspace: stripPersonalizationFromWorkspace$1,
64168
64288
  };
64169
64289
 
64170
64290
  /**
@@ -64315,6 +64435,128 @@ var registryApiController$3 = {
64315
64435
  REGISTRY_BASE_URL,
64316
64436
  };
64317
64437
 
64438
+ /**
64439
+ * widgetPublishManifest.js
64440
+ *
64441
+ * Pure helpers for widget-publish flow — version bumping, package-name
64442
+ * parsing, and manifest generation. No electron / fs / adm-zip deps so
64443
+ * these can be unit-tested directly.
64444
+ */
64445
+
64446
+ var widgetPublishManifest;
64447
+ var hasRequiredWidgetPublishManifest;
64448
+
64449
+ function requireWidgetPublishManifest () {
64450
+ if (hasRequiredWidgetPublishManifest) return widgetPublishManifest;
64451
+ hasRequiredWidgetPublishManifest = 1;
64452
+ const SEMVER_RE =
64453
+ /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
64454
+
64455
+ function bumpVersion(current, type) {
64456
+ if (!current || typeof current !== "string") return "1.0.0";
64457
+ const match = current.match(SEMVER_RE);
64458
+ if (!match) return current;
64459
+ let [, major, minor, patch] = match;
64460
+ major = Number(major);
64461
+ minor = Number(minor);
64462
+ patch = Number(patch);
64463
+ switch (type) {
64464
+ case "major":
64465
+ return `${major + 1}.0.0`;
64466
+ case "minor":
64467
+ return `${major}.${minor + 1}.0`;
64468
+ case "patch":
64469
+ default:
64470
+ return `${major}.${minor}.${patch + 1}`;
64471
+ }
64472
+ }
64473
+
64474
+ function resolveNextVersion(currentVersion, options = {}) {
64475
+ if (options.version) return options.version;
64476
+ if (options.bump) return bumpVersion(currentVersion, options.bump);
64477
+ return currentVersion;
64478
+ }
64479
+
64480
+ function parsePackageName(pkgName) {
64481
+ if (!pkgName) return { scope: null, name: "" };
64482
+ const m = pkgName.match(/^@([^/]+)\/(.+)$/);
64483
+ if (m) return { scope: m[1], name: m[2] };
64484
+ return { scope: null, name: pkgName };
64485
+ }
64486
+
64487
+ function generateWidgetRegistryManifest(
64488
+ packageJson,
64489
+ widgetConfigs,
64490
+ options = {},
64491
+ ) {
64492
+ const parsed = parsePackageName(packageJson.name || "");
64493
+ const scope = options.scope || parsed.scope || "";
64494
+ const name = options.name || parsed.name || packageJson.name || "";
64495
+ const version = options.version || packageJson.version || "1.0.0";
64496
+ const visibility = options.visibility === "private" ? "private" : "public";
64497
+
64498
+ const providerKeys = new Set();
64499
+ const providers = [];
64500
+ for (const cfg of widgetConfigs || []) {
64501
+ if (!Array.isArray(cfg.providers)) continue;
64502
+ for (const p of cfg.providers) {
64503
+ const key = `${p.type}:${p.providerClass || "mcp"}`;
64504
+ if (providerKeys.has(key)) continue;
64505
+ providerKeys.add(key);
64506
+ providers.push({
64507
+ type: p.type,
64508
+ required: p.required !== false,
64509
+ providerClass: p.providerClass || "mcp",
64510
+ });
64511
+ }
64512
+ }
64513
+
64514
+ const widgets = (widgetConfigs || []).map((cfg) => ({
64515
+ name: cfg.component || cfg.name,
64516
+ displayName: cfg.name || cfg.component,
64517
+ description: cfg.description || "",
64518
+ icon: cfg.icon || "square",
64519
+ providers: Array.isArray(cfg.providers)
64520
+ ? cfg.providers.map((p) => ({
64521
+ type: p.type,
64522
+ required: p.required !== false,
64523
+ providerClass: p.providerClass || "mcp",
64524
+ }))
64525
+ : [],
64526
+ }));
64527
+
64528
+ return {
64529
+ scope,
64530
+ name,
64531
+ displayName: options.displayName || packageJson.displayName || name,
64532
+ version,
64533
+ type: "widget",
64534
+ visibility,
64535
+ description: options.description || packageJson.description || "",
64536
+ author:
64537
+ options.authorName ||
64538
+ (typeof packageJson.author === "string"
64539
+ ? packageJson.author
64540
+ : packageJson.author?.name || ""),
64541
+ category: options.category || "general",
64542
+ tags: Array.isArray(options.tags) ? options.tags : [],
64543
+ icon: options.icon || "puzzle-piece",
64544
+ providers,
64545
+ widgets,
64546
+ appOrigin: options.appOrigin || "",
64547
+ publishedAt: new Date().toISOString(),
64548
+ };
64549
+ }
64550
+
64551
+ widgetPublishManifest = {
64552
+ bumpVersion,
64553
+ resolveNextVersion,
64554
+ parsePackageName,
64555
+ generateWidgetRegistryManifest,
64556
+ };
64557
+ return widgetPublishManifest;
64558
+ }
64559
+
64318
64560
  /**
64319
64561
  * themeRegistryController.js
64320
64562
  *
@@ -64359,14 +64601,20 @@ function generateThemeRegistryManifest(themeData, themeKey, options = {}) {
64359
64601
  const sanitizedName = sanitizeName(humanName);
64360
64602
  const colors = extractColors(themeData);
64361
64603
  const visibility = options.visibility === "private" ? "private" : "public";
64604
+ // Prefer an explicitly-resolved version (caller already bumped),
64605
+ // then the theme's own stored version, then the 1.0.0 baseline.
64606
+ // The old hardcoded 1.0.0 meant republishes silently clobbered the
64607
+ // registry record — update notifications never fired downstream.
64608
+ const version = options.version || themeData.version || "1.0.0";
64362
64609
 
64363
64610
  return {
64364
64611
  scope: options.scope || "",
64365
64612
  name: sanitizedName,
64366
64613
  displayName: humanName,
64367
- author: options.authorName || "",
64614
+ author:
64615
+ options.authorName || themeData.author || options.fallbackAuthor || "",
64368
64616
  description: options.description || "",
64369
- version: "1.0.0",
64617
+ version,
64370
64618
  visibility,
64371
64619
  type: "theme",
64372
64620
  category: "general",
@@ -64436,6 +64684,7 @@ function extractColors(themeData) {
64436
64684
  */
64437
64685
  async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
64438
64686
  try {
64687
+ const { resolveNextVersion } = requireWidgetPublishManifest();
64439
64688
  // Read the theme data
64440
64689
  const themesResult = themeController$3.listThemesForApplication(win, appId);
64441
64690
  if (themesResult.error) {
@@ -64469,11 +64718,33 @@ async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
64469
64718
  };
64470
64719
  }
64471
64720
 
64721
+ // Resolve version: prefer explicit, then bump the theme's stored
64722
+ // version, then start at 1.0.0. Without this themes always
64723
+ // published as 1.0.0 and update-check could never diff.
64724
+ const previousVersion = themeData.version || "1.0.0";
64725
+ const nextVersion = resolveNextVersion(previousVersion, {
64726
+ bump: options.bump,
64727
+ version: options.version,
64728
+ });
64729
+
64730
+ // Author fallback chain (F7): explicit → theme data → registry
64731
+ // profile displayName/username → blank. Matches the widget
64732
+ // author-normalization shape so ai-built / scaffolded themes
64733
+ // don't ship to the registry with a blank author field.
64734
+ const resolvedAuthor =
64735
+ options.authorName ||
64736
+ themeData.author ||
64737
+ profile?.displayName ||
64738
+ profile?.username ||
64739
+ "";
64740
+
64472
64741
  // Generate manifest
64473
64742
  const manifest = generateThemeRegistryManifest(themeData, themeKey, {
64474
64743
  ...options,
64475
64744
  scope,
64476
64745
  appOrigin: appId,
64746
+ version: nextVersion,
64747
+ authorName: resolvedAuthor,
64477
64748
  });
64478
64749
 
64479
64750
  // Validate colors
@@ -64530,6 +64801,35 @@ async function prepareThemeForPublish$1(win, appId, themeKey, options = {}) {
64530
64801
  "[ThemeRegistryController] Registry publish result:",
64531
64802
  registryResult,
64532
64803
  );
64804
+ // Persist the resolved version + author back onto the theme so
64805
+ // the NEXT publish picks up from here. Without this, the
64806
+ // publisher would be bumping from 1.0.0 every time and the
64807
+ // manifest's author normalization would be re-applied every
64808
+ // run (OK but confusing).
64809
+ if (registryResult?.success) {
64810
+ try {
64811
+ const updatedTheme = {
64812
+ ...themeData,
64813
+ version: nextVersion,
64814
+ author: resolvedAuthor || themeData.author,
64815
+ _registryMeta: {
64816
+ ...(themeData._registryMeta || {}),
64817
+ packageName: `${scope}/${manifest.name}`,
64818
+ scope,
64819
+ lastPublishedAt: new Date().toISOString(),
64820
+ lastPublishedVersion: nextVersion,
64821
+ },
64822
+ };
64823
+ themeController$3.saveThemeForApplication(win, appId, {
64824
+ key: themeKey,
64825
+ theme: updatedTheme,
64826
+ });
64827
+ } catch (persistErr) {
64828
+ console.warn(
64829
+ `[ThemeRegistryController] Version persist failed (continuing): ${persistErr.message}`,
64830
+ );
64831
+ }
64832
+ }
64533
64833
  }
64534
64834
 
64535
64835
  return {
@@ -64870,12 +65170,120 @@ function getThemePublishPreview$1(appId, themeKey) {
64870
65170
  }
64871
65171
  }
64872
65172
 
65173
+ /**
65174
+ * Check installed themes for available updates against the registry.
65175
+ *
65176
+ * Reads every theme from the app's theme file, picks the ones that
65177
+ * carry a `_registryMeta.packageName` (i.e. were installed from the
65178
+ * registry, not locally created), resolves each against the registry
65179
+ * index by `@scope/name` (with a bare-name fallback), and returns a
65180
+ * diff record for each stale theme.
65181
+ *
65182
+ * Mirrors `checkDashboardUpdatesForApp` — callable standalone, works
65183
+ * the same way on the renderer side.
65184
+ *
65185
+ * @param {BrowserWindow} win
65186
+ * @param {string} appId
65187
+ * @returns {Promise<{success, updates, totalInstalled, error?}>}
65188
+ */
65189
+ async function checkThemeUpdatesForApp$1(win, appId) {
65190
+ try {
65191
+ const { fetchRegistryIndex } = registryController$3;
65192
+ const themesResult = themeController$3.listThemesForApplication(win, appId);
65193
+ if (themesResult.error) {
65194
+ return {
65195
+ success: false,
65196
+ error: themesResult.message || "Failed to read themes",
65197
+ updates: [],
65198
+ };
65199
+ }
65200
+ const themes = themesResult.themes || {};
65201
+
65202
+ // Filter to registry-installed themes only.
65203
+ const installed = [];
65204
+ for (const [themeKey, themeData] of Object.entries(themes)) {
65205
+ const meta = themeData?._registryMeta;
65206
+ if (!meta?.packageName) continue;
65207
+ installed.push({
65208
+ themeKey,
65209
+ packageName: meta.packageName,
65210
+ scope: meta.scope || null,
65211
+ version: themeData.version || meta.lastPublishedVersion || "0.0.0",
65212
+ });
65213
+ }
65214
+
65215
+ if (installed.length === 0) {
65216
+ return { success: true, updates: [], totalInstalled: 0 };
65217
+ }
65218
+
65219
+ const index = await fetchRegistryIndex();
65220
+ const packages = (index.packages || []).filter(
65221
+ (p) => (p.type || "widget") === "theme",
65222
+ );
65223
+
65224
+ // Index registry packages by scoped + bare key, same pattern as
65225
+ // dashboard update check.
65226
+ const registryByKey = new Map();
65227
+ for (const pkg of packages) {
65228
+ if (!pkg.name) continue;
65229
+ if (pkg.scope) {
65230
+ const bareScope = String(pkg.scope).replace(/^@/, "");
65231
+ registryByKey.set(`@${bareScope}/${pkg.name}`, pkg);
65232
+ }
65233
+ registryByKey.set(pkg.name, pkg);
65234
+ }
65235
+
65236
+ const updates = [];
65237
+ for (const inst of installed) {
65238
+ const scope = inst.scope
65239
+ ? String(inst.scope).replace(/^@/, "")
65240
+ : inst.packageName.startsWith("@")
65241
+ ? inst.packageName.slice(1).split("/")[0]
65242
+ : null;
65243
+ const bareName = inst.packageName.includes("/")
65244
+ ? inst.packageName.split("/").pop()
65245
+ : inst.packageName;
65246
+ const scopedKey = scope ? `@${scope}/${bareName}` : null;
65247
+ const registryPkg =
65248
+ (scopedKey && registryByKey.get(scopedKey)) ||
65249
+ registryByKey.get(inst.packageName) ||
65250
+ registryByKey.get(bareName);
65251
+ if (!registryPkg) continue;
65252
+
65253
+ const latestVersion = registryPkg.version || "0.0.0";
65254
+ if (inst.version !== latestVersion) {
65255
+ updates.push({
65256
+ themeKey: inst.themeKey,
65257
+ packageName: inst.packageName,
65258
+ scope,
65259
+ installedVersion: inst.version,
65260
+ latestVersion,
65261
+ downloadUrl: registryPkg.downloadUrl || null,
65262
+ });
65263
+ }
65264
+ }
65265
+
65266
+ return {
65267
+ success: true,
65268
+ updates,
65269
+ totalInstalled: installed.length,
65270
+ };
65271
+ } catch (err) {
65272
+ console.error(
65273
+ "[ThemeRegistryController] Error checking theme updates:",
65274
+ err,
65275
+ );
65276
+ return { success: false, error: err.message, updates: [] };
65277
+ }
65278
+ }
65279
+
64873
65280
  var themeRegistryController$1 = {
64874
65281
  prepareThemeForPublish: prepareThemeForPublish$1,
64875
65282
  installThemeFromRegistry: installThemeFromRegistry$1,
64876
65283
  getThemePublishPreview: getThemePublishPreview$1,
64877
65284
  generateThemeRegistryManifest,
64878
65285
  extractColors,
65286
+ checkThemeUpdatesForApp: checkThemeUpdatesForApp$1,
64879
65287
  };
64880
65288
 
64881
65289
  /**
@@ -64910,6 +65318,7 @@ const {
64910
65318
  buildWidgetDependencies,
64911
65319
  buildProviderRequirements,
64912
65320
  applyEventWiringToLayout,
65321
+ stripPersonalizationFromWorkspace,
64913
65322
  } = dashboardConfigUtils$1;
64914
65323
  const { searchRegistry, getPackage } = registryController$3;
64915
65324
  const { getStoredToken, clearToken } = registryAuthController$2;
@@ -64957,7 +65366,11 @@ async function exportDashboardConfig$1(
64957
65366
  };
64958
65367
  }
64959
65368
 
64960
- const layout = workspace.layout || [];
65369
+ // Strip publisher-specific personalization (userPrefs,
65370
+ // selectedProviders) so the exported file carries a clean
65371
+ // template, not one pre-filled with the publisher's paths.
65372
+ const sharedWorkspace = stripPersonalizationFromWorkspace(workspace);
65373
+ const layout = sharedWorkspace.layout || [];
64961
65374
 
64962
65375
  // 2. Collect components, extract wiring, resolve deps — walk main
64963
65376
  // layout, every page, and the sidebar so multi-page / sidebar
@@ -64985,13 +65398,17 @@ async function exportDashboardConfig$1(
64985
65398
  label: workspace.label || workspace.name,
64986
65399
  version: workspace.version || 1,
64987
65400
  layout,
64988
- ...(Array.isArray(workspace.pages) && workspace.pages.length > 0
64989
- ? { pages: workspace.pages, activePageId: workspace.activePageId }
65401
+ ...(Array.isArray(sharedWorkspace.pages) &&
65402
+ sharedWorkspace.pages.length > 0
65403
+ ? {
65404
+ pages: sharedWorkspace.pages,
65405
+ activePageId: workspace.activePageId,
65406
+ }
64990
65407
  : {}),
64991
- ...(Array.isArray(workspace.sidebarLayout) &&
64992
- workspace.sidebarLayout.length > 0
65408
+ ...(Array.isArray(sharedWorkspace.sidebarLayout) &&
65409
+ sharedWorkspace.sidebarLayout.length > 0
64993
65410
  ? {
64994
- sidebarLayout: workspace.sidebarLayout,
65411
+ sidebarLayout: sharedWorkspace.sidebarLayout,
64995
65412
  sidebarEnabled: workspace.sidebarEnabled !== false,
64996
65413
  }
64997
65414
  : {}),
@@ -65331,7 +65748,30 @@ async function processDashboardConfig(
65331
65748
  dashboardConfig.widgets.length
65332
65749
  ) {
65333
65750
  const installedWidgets = widgetRegistry.getWidgets();
65334
- const installedPackages = new Set(installedWidgets.map((w) => w.name));
65751
+ // Build a canonical id set — `@scope/name` — so lookups survive
65752
+ // the publisher/installer having the widget under different
65753
+ // keys. The widget-registry entry carries `w.packageId`,
65754
+ // `w.name`, and `w.scope`; the dashboard dep carries
65755
+ // `dep.package` (now scope-remapped at publish time). Seeding
65756
+ // every form we've seen avoids silent "missing widget" misses
65757
+ // on bare-vs-scoped mismatch.
65758
+ const canonicalId = (w) => {
65759
+ if (w?.packageId) return w.packageId;
65760
+ if (w?.scope && w?.name) {
65761
+ const scope = String(w.scope).replace(/^@/, "");
65762
+ const bareName = String(w.name).replace(new RegExp(`^@?${scope}/`), "");
65763
+ return `@${scope}/${bareName}`;
65764
+ }
65765
+ return w?.name || null;
65766
+ };
65767
+ const installedPackages = new Set();
65768
+ for (const w of installedWidgets) {
65769
+ const id = canonicalId(w);
65770
+ if (id) installedPackages.add(id);
65771
+ // Back-compat: keep bare name in the set too, so older
65772
+ // dashboard configs (pre-scope-remap) still match.
65773
+ if (w?.name) installedPackages.add(w.name);
65774
+ }
65335
65775
 
65336
65776
  // Emit initial "pending" state for all widgets
65337
65777
  for (let i = 0; i < widgetTotal; i++) {
@@ -65369,7 +65809,20 @@ async function processDashboardConfig(
65369
65809
  const displayName =
65370
65810
  widgetDep.displayName || widgetDep.name || packageName;
65371
65811
 
65372
- if (installedPackages.has(packageName)) {
65812
+ // Try both the fully-scoped id and a bare-name fallback — the
65813
+ // installed set holds both forms, but the dashboard dep may
65814
+ // carry either shape. Without this, a widget stored locally as
65815
+ // `@trops/pipeline` and referenced as `pipeline` (or vice
65816
+ // versa) silently flagged as "failed" even though it was
65817
+ // installed.
65818
+ const isInstalled =
65819
+ installedPackages.has(packageName) ||
65820
+ (packageName?.includes("/") &&
65821
+ installedPackages.has(packageName.split("/").pop())) ||
65822
+ (packageName &&
65823
+ !packageName.startsWith("@") &&
65824
+ installedPackages.has(`@${packageName}`));
65825
+ if (isInstalled) {
65373
65826
  installSummary.alreadyInstalled.push(packageName);
65374
65827
  win.webContents.send(DASHBOARD_CONFIG_INSTALL_PROGRESS, {
65375
65828
  packageName,
@@ -65401,6 +65854,10 @@ async function processDashboardConfig(
65401
65854
  );
65402
65855
  installSummary.installed.push({ packageName, config });
65403
65856
  installedPackages.add(packageName);
65857
+ // Also add the canonical form so a subsequent dep that
65858
+ // references it under a different shape still hits.
65859
+ const installedCanonical = canonicalId(config);
65860
+ if (installedCanonical) installedPackages.add(installedCanonical);
65404
65861
  win.webContents.send(DASHBOARD_CONFIG_INSTALL_PROGRESS, {
65405
65862
  packageName,
65406
65863
  displayName,
@@ -66138,16 +66595,30 @@ async function getDashboardPublishPlan$1(
66138
66595
  }
66139
66596
  }
66140
66597
 
66598
+ // Scopes that only exist on the publisher's machine and aren't
66599
+ // resolvable from the registry as-is. Any dep under one of these
66600
+ // scopes MUST be republished under the caller's scope for the
66601
+ // dashboard's widget refs to work on another machine. The modal
66602
+ // uses this flag to auto-check + lock such rows.
66603
+ const LOCAL_ONLY_SCOPES = new Set(["ai-built", "@ai-built"]);
66604
+
66141
66605
  const widgets = deps.widgets.map((w) => {
66142
66606
  const publishScope = publishScopeFor(w);
66143
66607
  const key =
66144
66608
  publishScope && w.packageName
66145
66609
  ? `${publishScope}/${w.packageName}`
66146
66610
  : null;
66611
+ const isLocalOnlyScope =
66612
+ !!w.scope && LOCAL_ONLY_SCOPES.has(String(w.scope).replace(/^@/, ""));
66147
66613
  return {
66148
66614
  ...w,
66149
66615
  publishScope,
66150
66616
  registry: key ? resolvedByKey.get(key) || null : null,
66617
+ // True when this widget cannot be installed as-is under its
66618
+ // local scope — the dashboard publish MUST republish it under
66619
+ // the caller's scope alongside the dashboard itself. The
66620
+ // modal treats this as mandatory rather than opt-in.
66621
+ requiresRepublish: isLocalOnlyScope,
66151
66622
  };
66152
66623
  });
66153
66624
 
@@ -66211,6 +66682,7 @@ async function prepareDashboardForPublish$1(
66211
66682
  const {
66212
66683
  generateRegistryManifest,
66213
66684
  } = dashboardConfigUtils$1;
66685
+ const { resolveNextVersion } = requireWidgetPublishManifest();
66214
66686
 
66215
66687
  // 1. Read workspace
66216
66688
  const filename = path$2.join(
@@ -66243,7 +66715,29 @@ async function prepareDashboardForPublish$1(
66243
66715
  };
66244
66716
  }
66245
66717
 
66246
- const layout = workspace.layout || [];
66718
+ // Resolve the version this publish will ship. Previous publishes
66719
+ // store the last version on workspace._dashboardConfig.version; new
66720
+ // dashboards start at 1.0.0. Caller may pass `options.version`
66721
+ // (explicit) or `options.bump` ("patch"/"minor"/"major"). Without
66722
+ // this, every dashboard republish used a hardcoded 1.0.0 — the
66723
+ // registry never saw a new version, so the update-check never
66724
+ // fired a notification for installers.
66725
+ const previousVersion = workspace._dashboardConfig?.version || "1.0.0";
66726
+ const nextVersion = resolveNextVersion(previousVersion, {
66727
+ bump: options.bump,
66728
+ version: options.version,
66729
+ });
66730
+
66731
+ // Strip publisher-specific personalization (userPrefs,
66732
+ // selectedProviders) from every widget instance before we snapshot
66733
+ // the workspace into the dashboardConfig. Without this, every
66734
+ // installer inherits the publisher's absolute filesystem paths,
66735
+ // region tags, and provider bindings as their "defaults" — a
66736
+ // widget's own `defaultValue` on each field never gets a chance.
66737
+ // Layout position, ordering, nested containers, and any title text
66738
+ // are preserved (they're part of the template, not personal).
66739
+ const sharedWorkspace = stripPersonalizationFromWorkspace(workspace);
66740
+ const layout = sharedWorkspace.layout || [];
66247
66741
 
66248
66742
  // 3. Build the dashboard config — walk main + pages + sidebar
66249
66743
  const componentNames = collectComponentNamesFromWorkspace(workspace);
@@ -66270,6 +66764,10 @@ async function prepareDashboardForPublish$1(
66270
66764
  schemaVersion: CURRENT_SCHEMA_VERSION,
66271
66765
  name: workspace.name || workspace.label || "Dashboard",
66272
66766
  description: options.description || "",
66767
+ // Package version (semver). Distinct from workspace.version
66768
+ // (schema revision, integer). Persisted on the workspace after a
66769
+ // successful publish so the next publish resolves from here.
66770
+ version: nextVersion,
66273
66771
  ...(options.authorName
66274
66772
  ? { author: { name: options.authorName, id: options.authorId || "" } }
66275
66773
  : {}),
@@ -66283,13 +66781,17 @@ async function prepareDashboardForPublish$1(
66283
66781
  label: workspace.label || workspace.name,
66284
66782
  version: workspace.version || 1,
66285
66783
  layout,
66286
- ...(Array.isArray(workspace.pages) && workspace.pages.length > 0
66287
- ? { pages: workspace.pages, activePageId: workspace.activePageId }
66784
+ ...(Array.isArray(sharedWorkspace.pages) &&
66785
+ sharedWorkspace.pages.length > 0
66786
+ ? {
66787
+ pages: sharedWorkspace.pages,
66788
+ activePageId: workspace.activePageId,
66789
+ }
66288
66790
  : {}),
66289
- ...(Array.isArray(workspace.sidebarLayout) &&
66290
- workspace.sidebarLayout.length > 0
66791
+ ...(Array.isArray(sharedWorkspace.sidebarLayout) &&
66792
+ sharedWorkspace.sidebarLayout.length > 0
66291
66793
  ? {
66292
- sidebarLayout: workspace.sidebarLayout,
66794
+ sidebarLayout: sharedWorkspace.sidebarLayout,
66293
66795
  sidebarEnabled: workspace.sidebarEnabled !== false,
66294
66796
  }
66295
66797
  : {}),
@@ -66420,6 +66922,7 @@ async function prepareDashboardForPublish$1(
66420
66922
  repository: options.repository || "",
66421
66923
  appOrigin: appId,
66422
66924
  visibility: options.visibility || "public",
66925
+ version: nextVersion,
66423
66926
  });
66424
66927
 
66425
66928
  // 9. Show save dialog for the publish package
@@ -66467,6 +66970,35 @@ async function prepareDashboardForPublish$1(
66467
66970
  console.log(
66468
66971
  `[DashboardConfigController] Published to registry: ${registrySubmission.registryUrl}`,
66469
66972
  );
66973
+ // Persist the resolved next version back onto the workspace
66974
+ // so the NEXT publish resolves from it. Without this, the
66975
+ // publisher would have to re-enter the same version every
66976
+ // time (or keep bumping from 1.0.0, masking that the
66977
+ // registry already advanced).
66978
+ try {
66979
+ const workspaceController = workspaceController_1;
66980
+ const nextWorkspace = {
66981
+ ...workspace,
66982
+ _dashboardConfig: {
66983
+ ...(workspace._dashboardConfig || {}),
66984
+ version: nextVersion,
66985
+ registryPackage: manifest.name,
66986
+ registryScope: manifest.scope || manifest.githubUser,
66987
+ },
66988
+ };
66989
+ workspaceController.saveWorkspaceForApplication(
66990
+ win,
66991
+ appId,
66992
+ nextWorkspace,
66993
+ );
66994
+ } catch (persistErr) {
66995
+ // Non-fatal — registry is the source of truth for
66996
+ // latestVersion, so the next publish can still resolve
66997
+ // against it if this workspace write fails.
66998
+ console.warn(
66999
+ `[DashboardConfigController] Version persistence failed (continuing): ${persistErr.message}`,
67000
+ );
67001
+ }
66470
67002
  } else {
66471
67003
  console.warn(
66472
67004
  `[DashboardConfigController] Registry publish failed: ${registrySubmission.error}`,
@@ -72931,120 +73463,6 @@ var dashboardRatingsController = {
72931
73463
  enrichPackagesWithRatings: enrichPackagesWithRatings$1,
72932
73464
  };
72933
73465
 
72934
- /**
72935
- * widgetPublishManifest.js
72936
- *
72937
- * Pure helpers for widget-publish flow — version bumping, package-name
72938
- * parsing, and manifest generation. No electron / fs / adm-zip deps so
72939
- * these can be unit-tested directly.
72940
- */
72941
-
72942
- const SEMVER_RE =
72943
- /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
72944
-
72945
- function bumpVersion(current, type) {
72946
- if (!current || typeof current !== "string") return "1.0.0";
72947
- const match = current.match(SEMVER_RE);
72948
- if (!match) return current;
72949
- let [, major, minor, patch] = match;
72950
- major = Number(major);
72951
- minor = Number(minor);
72952
- patch = Number(patch);
72953
- switch (type) {
72954
- case "major":
72955
- return `${major + 1}.0.0`;
72956
- case "minor":
72957
- return `${major}.${minor + 1}.0`;
72958
- case "patch":
72959
- default:
72960
- return `${major}.${minor}.${patch + 1}`;
72961
- }
72962
- }
72963
-
72964
- function resolveNextVersion$1(currentVersion, options = {}) {
72965
- if (options.version) return options.version;
72966
- if (options.bump) return bumpVersion(currentVersion, options.bump);
72967
- return currentVersion;
72968
- }
72969
-
72970
- function parsePackageName$1(pkgName) {
72971
- if (!pkgName) return { scope: null, name: "" };
72972
- const m = pkgName.match(/^@([^/]+)\/(.+)$/);
72973
- if (m) return { scope: m[1], name: m[2] };
72974
- return { scope: null, name: pkgName };
72975
- }
72976
-
72977
- function generateWidgetRegistryManifest$1(
72978
- packageJson,
72979
- widgetConfigs,
72980
- options = {},
72981
- ) {
72982
- const parsed = parsePackageName$1(packageJson.name || "");
72983
- const scope = options.scope || parsed.scope || "";
72984
- const name = options.name || parsed.name || packageJson.name || "";
72985
- const version = options.version || packageJson.version || "1.0.0";
72986
- const visibility = options.visibility === "private" ? "private" : "public";
72987
-
72988
- const providerKeys = new Set();
72989
- const providers = [];
72990
- for (const cfg of widgetConfigs || []) {
72991
- if (!Array.isArray(cfg.providers)) continue;
72992
- for (const p of cfg.providers) {
72993
- const key = `${p.type}:${p.providerClass || "mcp"}`;
72994
- if (providerKeys.has(key)) continue;
72995
- providerKeys.add(key);
72996
- providers.push({
72997
- type: p.type,
72998
- required: p.required !== false,
72999
- providerClass: p.providerClass || "mcp",
73000
- });
73001
- }
73002
- }
73003
-
73004
- const widgets = (widgetConfigs || []).map((cfg) => ({
73005
- name: cfg.component || cfg.name,
73006
- displayName: cfg.name || cfg.component,
73007
- description: cfg.description || "",
73008
- icon: cfg.icon || "square",
73009
- providers: Array.isArray(cfg.providers)
73010
- ? cfg.providers.map((p) => ({
73011
- type: p.type,
73012
- required: p.required !== false,
73013
- providerClass: p.providerClass || "mcp",
73014
- }))
73015
- : [],
73016
- }));
73017
-
73018
- return {
73019
- scope,
73020
- name,
73021
- displayName: options.displayName || packageJson.displayName || name,
73022
- version,
73023
- type: "widget",
73024
- visibility,
73025
- description: options.description || packageJson.description || "",
73026
- author:
73027
- options.authorName ||
73028
- (typeof packageJson.author === "string"
73029
- ? packageJson.author
73030
- : packageJson.author?.name || ""),
73031
- category: options.category || "general",
73032
- tags: Array.isArray(options.tags) ? options.tags : [],
73033
- icon: options.icon || "puzzle-piece",
73034
- providers,
73035
- widgets,
73036
- appOrigin: options.appOrigin || "",
73037
- publishedAt: new Date().toISOString(),
73038
- };
73039
- }
73040
-
73041
- var widgetPublishManifest = {
73042
- bumpVersion,
73043
- resolveNextVersion: resolveNextVersion$1,
73044
- parsePackageName: parsePackageName$1,
73045
- generateWidgetRegistryManifest: generateWidgetRegistryManifest$1,
73046
- };
73047
-
73048
73466
  /**
73049
73467
  * widgetRegistryController.js
73050
73468
  *
@@ -73074,7 +73492,7 @@ const {
73074
73492
  resolveNextVersion,
73075
73493
  parsePackageName,
73076
73494
  generateWidgetRegistryManifest,
73077
- } = widgetPublishManifest;
73495
+ } = requireWidgetPublishManifest();
73078
73496
 
73079
73497
  /**
73080
73498
  * Resilient widget lookup. Callers pass identifiers in different shapes —
@@ -73550,6 +73968,7 @@ const {
73550
73968
  prepareThemeForPublish,
73551
73969
  installThemeFromRegistry,
73552
73970
  getThemePublishPreview,
73971
+ checkThemeUpdatesForApp,
73553
73972
  } = themeRegistryController$1;
73554
73973
  const {
73555
73974
  prepareWidgetForPublish,
@@ -73643,6 +74062,7 @@ var controller = {
73643
74062
  prepareThemeForPublish,
73644
74063
  installThemeFromRegistry,
73645
74064
  getThemePublishPreview,
74065
+ checkThemeUpdatesForApp,
73646
74066
  prepareWidgetForPublish,
73647
74067
  inspectWidgetPackage,
73648
74068
  assignRoles,