@peers-app/peers-ui 0.18.8 → 0.19.6

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.
@@ -11,56 +11,119 @@ const tooltip_1 = require("../../components/tooltip");
11
11
  const globals_1 = require("../../globals");
12
12
  const hooks_1 = require("../../hooks");
13
13
  const ui_loader_1 = require("../../ui-router/ui-loader");
14
- const PackageVersionBadge = ({ activePackageVersionId, packageId, followVersionTags, }) => {
15
- const data = (0, hooks_1.usePromise)(async () => {
16
- if (!activePackageVersionId)
14
+ const package_helpers_1 = require("./package-helpers");
15
+ function tagBadgeClass(tag) {
16
+ if (tag === "dev")
17
+ return "text-bg-danger";
18
+ if (tag.startsWith("beta"))
19
+ return "text-bg-warning";
20
+ return "text-bg-success";
21
+ }
22
+ function updateBadgeClass(level) {
23
+ if (level === "major")
24
+ return "text-bg-danger";
25
+ if (level === "minor")
26
+ return "text-bg-warning";
27
+ return "text-bg-info";
28
+ }
29
+ const PackageRow = ({ pkg, onUpdated }) => {
30
+ const [updating, setUpdating] = (0, react_1.useState)(false);
31
+ const [devicePrefs] = (0, hooks_1.useObservable)((0, peers_sdk_1.packagePrefsVar)(pkg.packageId));
32
+ const effective = (0, peers_sdk_1.getEffectivePackagePrefs)(pkg, devicePrefs);
33
+ const status = (0, hooks_1.usePromise)(async () => {
34
+ if (!effective.activePackageVersionId)
17
35
  return null;
18
- const all = await (0, peers_sdk_1.PackageVersions)().list({ packageId });
19
- const active = all.find((v) => v.packageVersionId === activePackageVersionId);
20
- if (!active)
36
+ const s = await (0, package_helpers_1.checkVersionStatus)(pkg.packageId, effective.activePackageVersionId, effective.followTags);
37
+ if (!s)
21
38
  return null;
22
- let newerLevel = null;
23
- if (active.version) {
24
- const parse = (v) => v.split(".").map(Number);
25
- const [aMaj, aMin, aPat] = parse(active.version);
26
- for (const v of all) {
27
- if (!v.version || v.packageVersionId === activePackageVersionId)
28
- continue;
29
- if (!(0, peers_sdk_1.doesTagMatch)(active.versionTag, v.versionTag, followVersionTags, undefined))
30
- continue;
31
- const [maj, min, pat] = parse(v.version);
32
- if (maj > aMaj) {
33
- newerLevel = "major";
34
- break;
35
- }
36
- if (maj === aMaj && min > aMin) {
37
- newerLevel = "minor";
38
- continue;
39
- }
40
- if (maj === aMaj && min === aMin && pat > aPat && !newerLevel) {
41
- newerLevel = "patch";
39
+ let groupReleasePv = null;
40
+ if (s.activePv.versionTag === "dev" &&
41
+ pkg.activePackageVersionId &&
42
+ pkg.activePackageVersionId !== effective.activePackageVersionId) {
43
+ try {
44
+ const gpv = await (0, peers_sdk_1.PackageVersions)().get(pkg.activePackageVersionId);
45
+ if (gpv && gpv.versionTag !== "dev")
46
+ groupReleasePv = gpv;
47
+ }
48
+ catch { }
49
+ }
50
+ let latestDevPv = null;
51
+ if (s.activePv.versionTag !== "dev") {
52
+ const allVersions = await (0, peers_sdk_1.PackageVersions)().list({ packageId: pkg.packageId });
53
+ for (const v of allVersions) {
54
+ if (v.versionTag !== "dev")
42
55
  continue;
43
- }
44
- if (maj === aMaj &&
45
- min === aMin &&
46
- pat === aPat &&
47
- (v.createdAt || "") > (active.createdAt || "") &&
48
- !newerLevel) {
49
- newerLevel = "patch";
56
+ if (!latestDevPv || (v.createdAt || "") > (latestDevPv.createdAt || "")) {
57
+ latestDevPv = v;
50
58
  }
51
59
  }
52
60
  }
53
- return { pv: active, newerLevel };
54
- }, undefined, [activePackageVersionId, packageId, followVersionTags]);
55
- if (!data)
56
- return null;
57
- const { pv, newerLevel } = data;
58
- return ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", pv.version] }), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${pv.versionTag === "dev" ? "text-bg-danger" : pv.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"}`, style: { fontSize: "0.65em" }, children: pv.versionTag })), newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, style: { fontSize: "0.65em" }, children: [newerLevel, " update"] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] }));
61
+ return { ...s, groupReleasePv, latestDevPv };
62
+ }, undefined, [
63
+ effective.activePackageVersionId,
64
+ pkg.packageId,
65
+ effective.followTags,
66
+ pkg.activePackageVersionId,
67
+ ]);
68
+ const isOnDev = status?.activePv.versionTag === "dev";
69
+ const hasGroupRelease = !!status?.groupReleasePv;
70
+ const hasUpdate = !isOnDev && !!status?.newerLevel && !!status?.newestPv;
71
+ const hasDevVersion = !isOnDev && !!status?.latestDevPv;
72
+ const isDeviceOverridden = !!effective.activePackageVersionId &&
73
+ !!pkg.activePackageVersionId &&
74
+ effective.activePackageVersionId !== pkg.activePackageVersionId;
75
+ async function handleUpdate() {
76
+ if (!status?.newestPv)
77
+ return;
78
+ setUpdating(true);
79
+ try {
80
+ await (0, package_helpers_1.activatePackageVersion)(pkg.packageId, status.newestPv);
81
+ onUpdated();
82
+ }
83
+ catch (err) {
84
+ alert(`Failed to update: ${err instanceof Error ? err.message : String(err)}`);
85
+ }
86
+ finally {
87
+ setUpdating(false);
88
+ }
89
+ }
90
+ async function handleUseRelease() {
91
+ setUpdating(true);
92
+ try {
93
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, { activePackageVersionId: undefined });
94
+ onUpdated();
95
+ }
96
+ catch (err) {
97
+ alert(`Failed to switch: ${err instanceof Error ? err.message : String(err)}`);
98
+ }
99
+ finally {
100
+ setUpdating(false);
101
+ }
102
+ }
103
+ async function handleUseDev() {
104
+ if (!status?.latestDevPv)
105
+ return;
106
+ setUpdating(true);
107
+ try {
108
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, {
109
+ activePackageVersionId: status.latestDevPv.packageVersionId,
110
+ });
111
+ onUpdated();
112
+ }
113
+ catch (err) {
114
+ alert(`Failed to switch: ${err instanceof Error ? err.message : String(err)}`);
115
+ }
116
+ finally {
117
+ setUpdating(false);
118
+ }
119
+ }
120
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4 d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", style: { minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-fill" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#packages/${pkg.packageId}`, children: pkg.name }), isDeviceOverridden && ((0, jsx_runtime_1.jsx)("span", { className: "text-warning ms-1", title: "This device is using a different version than the group", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-pc-display" }) })), status && ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", status.activePv.version] }), status.activePv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${tagBadgeClass(status.activePv.versionTag)}`, style: { fontSize: "0.65em" }, children: status.activePv.versionTag })), hasUpdate ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 ${updateBadgeClass(status.newerLevel)}`, style: { fontSize: "0.65em" }, children: ["v", status.newestPv?.version, " available"] })) : isOnDev && hasGroupRelease ? ((0, jsx_runtime_1.jsxs)("span", { className: "badge ms-1 text-bg-secondary", style: { fontSize: "0.65em" }, children: ["release: v", status.groupReleasePv?.version] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] })), (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: pkg.description, positions: ["bottom", "top", "right", "left"] })] }), hasUpdate && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary ms-2 text-nowrap", disabled: updating, onClick: handleUpdate, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-up-circle me-1" })), "Update"] })), isOnDev && hasGroupRelease && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary ms-2 text-nowrap", disabled: updating, onClick: handleUseRelease, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right me-1" })), "Use Release"] })), hasDevVersion && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-danger ms-2 text-nowrap", disabled: updating, onClick: handleUseDev, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-tools me-1" })), "Use Dev"] }))] }));
59
121
  };
60
122
  const PackageList = () => {
61
123
  const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
62
124
  const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
63
125
  const addingPackage = (0, hooks_1.useObservableState)(false);
126
+ const refreshKey = (0, hooks_1.useObservableState)(0);
64
127
  const [cursorObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)());
65
128
  const newCursor = (0, react_1.useCallback)(async () => {
66
129
  const cursor = await (0, peers_sdk_1.Packages)().cursor({
@@ -91,6 +154,42 @@ const PackageList = () => {
91
154
  if (moreMatches.length === 0) {
92
155
  cursorObs(undefined);
93
156
  }
157
+ if (!searchText.length && existing.length === 0 && moreMatches.length > 0) {
158
+ const DEV_WITH_RELEASE_RANK = 0.5;
159
+ const rankMap = new Map();
160
+ await Promise.all(moreMatches.map(async (pkg) => {
161
+ try {
162
+ const pVar = (0, peers_sdk_1.packagePrefsVar)(pkg.packageId);
163
+ await pVar.loadingPromise;
164
+ const eff = (0, peers_sdk_1.getEffectivePackagePrefs)(pkg, pVar());
165
+ if (!eff.activePackageVersionId)
166
+ return;
167
+ const s = await (0, package_helpers_1.checkVersionStatus)(pkg.packageId, eff.activePackageVersionId, eff.followTags);
168
+ if (!s)
169
+ return;
170
+ if (s.newerLevel) {
171
+ rankMap.set(pkg.packageId, package_helpers_1.updateLevelRank[s.newerLevel]);
172
+ }
173
+ else if (s.activePv.versionTag === "dev" && pkg.activePackageVersionId) {
174
+ try {
175
+ const gpv = await (0, peers_sdk_1.PackageVersions)().get(pkg.activePackageVersionId);
176
+ if (gpv && gpv.versionTag !== "dev") {
177
+ rankMap.set(pkg.packageId, DEV_WITH_RELEASE_RANK);
178
+ }
179
+ }
180
+ catch { }
181
+ }
182
+ }
183
+ catch { }
184
+ }));
185
+ moreMatches.sort((a, b) => {
186
+ const aRank = rankMap.get(a.packageId) ?? 0;
187
+ const bRank = rankMap.get(b.packageId) ?? 0;
188
+ if (bRank !== aRank)
189
+ return bRank - aRank;
190
+ return (a.name || "").localeCompare(b.name || "");
191
+ });
192
+ }
94
193
  return moreMatches;
95
194
  }
96
195
  async function searchSubmit(evt) {
@@ -99,7 +198,6 @@ const PackageList = () => {
99
198
  const name = searchText.trim();
100
199
  if (!name)
101
200
  return;
102
- // check if name is a remote repo url
103
201
  if (name.startsWith("http") || name.endsWith(".git")) {
104
202
  if (!confirm(`Add remote package: ${name}`))
105
203
  return;
@@ -113,7 +211,6 @@ const PackageList = () => {
113
211
  }
114
212
  catch (err) {
115
213
  let errMessage = err instanceof Error ? err.message : String(err);
116
- // replace all whitespace with a single space for confirm dialog
117
214
  errMessage = errMessage.replace(/\s+/g, " ");
118
215
  confirm(`Error adding remote package: ${errMessage}`);
119
216
  }
@@ -142,10 +239,8 @@ const PackageList = () => {
142
239
  if (addingPackage()) {
143
240
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "container-fluid d-flex justify-content-center mt-5", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-seam" }), "\u00A0 Adding Package"] }), (0, jsx_runtime_1.jsx)("div", { className: "container-fluid d-flex justify-content-center mt-2", children: (0, jsx_runtime_1.jsx)("div", { className: "progress", role: "progressbar", "aria-label": "Animated striped example", "aria-valuenow": 100, "aria-valuemin": 0, "aria-valuemax": 100, children: (0, jsx_runtime_1.jsx)("div", { className: "progress-bar progress-bar-striped progress-bar-animated", style: { width: 150 } }) }) })] }));
144
241
  }
145
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-group mt-3 mb-3", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search, add, or create package", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary dropdown-toggle sm", type: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsxs)("ul", { className: "dropdown-menu dropdown-menu-end", children: [(0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", href: "#packages/newlocal", children: "New Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Remote Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", onClick: () => peers_sdk_1.rpcServerCalls.openPath(peers_sdk_1.packagesRootDir), style: { cursor: "pointer" }, children: "Open Packages Directory" }) })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: searchText, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (packages) => {
146
- return packages.map((pkg) => {
147
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-fill" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#packages/${pkg.packageId}`, children: pkg.name }), (0, jsx_runtime_1.jsx)(PackageVersionBadge, { activePackageVersionId: pkg.activePackageVersionId, packageId: pkg.packageId, followVersionTags: pkg.followVersionTags }), (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: pkg.description, positions: ["bottom", "top", "right", "left"] })] }, pkg.packageId));
148
- });
242
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-group mt-3 mb-3", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search, add, or create package", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary dropdown-toggle sm", type: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsxs)("ul", { className: "dropdown-menu dropdown-menu-end", children: [(0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", href: "#packages/newlocal", children: "New Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Remote Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", onClick: () => peers_sdk_1.rpcServerCalls.openPath(peers_sdk_1.packagesRootDir), style: { cursor: "pointer" }, children: "Open Packages Directory" }) })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: `${searchText}_${refreshKey()}`, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (packages) => {
243
+ return packages.map((pkg) => ((0, jsx_runtime_1.jsx)(PackageRow, { pkg: pkg, onUpdated: () => refreshKey(refreshKey() + 1) }, pkg.packageId)));
149
244
  }, loadingIndicator: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 }, children: (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}) }), endOfList: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 } }) }) })] }));
150
245
  };
151
246
  exports.PackageList = PackageList;
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const peers_sdk_1 = require("@peers-app/peers-sdk");
6
6
  const react_1 = require("react");
7
7
  const hooks_1 = require("../../hooks");
8
+ const package_helpers_1 = require("./package-helpers");
8
9
  function formatDate(iso) {
9
10
  try {
10
11
  const d = new Date(iso);
@@ -77,18 +78,7 @@ const PackageVersionsList = (props) => {
77
78
  async function activateVersion(pv) {
78
79
  setActivating(pv.packageVersionId);
79
80
  try {
80
- await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, {
81
- activePackageVersionId: pv.packageVersionId,
82
- });
83
- // For non-dev versions, also update the group default so other devices
84
- // can discover this as the recommended version.
85
- if (pv.versionTag !== "dev") {
86
- const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
87
- if (current) {
88
- current.activePackageVersionId = pv.packageVersionId;
89
- await (0, peers_sdk_1.Packages)().signAndSave(current);
90
- }
91
- }
81
+ await (0, package_helpers_1.activatePackageVersion)(pkg.packageId, pv);
92
82
  await pkg.load();
93
83
  refreshKey(refreshKey() + 1);
94
84
  }
@@ -125,7 +115,23 @@ const PackageVersionsList = (props) => {
125
115
  signature: "",
126
116
  });
127
117
  const pvHash = (0, peers_sdk_1.computePackageVersionHash)(pv.version, newTag, pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
128
- const updated = { ...pv, versionTag: newTag, packageVersionHash: pvHash, history };
118
+ const updated = {
119
+ ...pv,
120
+ versionTag: newTag,
121
+ packageVersionHash: pvHash,
122
+ history,
123
+ };
124
+ // Auto-sign with package author key if available (server-side, key never exposed)
125
+ const authorSig = await peers_sdk_1.rpcServerCalls.signPackageAuthorVersion({
126
+ packageId: updated.packageId,
127
+ packageVersionId: updated.packageVersionId,
128
+ version: updated.version,
129
+ versionTag: newTag,
130
+ packageBundleFileHash: updated.packageBundleFileHash,
131
+ routesBundleFileHash: updated.routesBundleFileHash,
132
+ uiBundleFileHash: updated.uiBundleFileHash,
133
+ });
134
+ updated.packageAuthorSignature = authorSig;
129
135
  await (0, peers_sdk_1.PackageVersions)().signAndSave(updated, { saveAsSnapshot: true });
130
136
  refreshKey(refreshKey() + 1);
131
137
  }
@@ -147,7 +153,17 @@ const PackageVersionsList = (props) => {
147
153
  async function pinVersion() {
148
154
  setPinning(true);
149
155
  try {
150
- await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, { pinned: true });
156
+ const pinsGroup = !(0, peers_sdk_1.hasDeviceFollowOverride)(devicePrefs) && (await (0, package_helpers_1.isGroupAdmin)());
157
+ if (pinsGroup) {
158
+ const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
159
+ if (current) {
160
+ current.versionFollowRange = "pinned";
161
+ await (0, peers_sdk_1.Packages)().signAndSave(current);
162
+ }
163
+ }
164
+ else {
165
+ await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, { pinned: true });
166
+ }
151
167
  await pkg.load();
152
168
  }
153
169
  catch (err) {
@@ -1,4 +1,5 @@
1
- import type { IAppNav, IPackage } from "@peers-app/peers-sdk";
1
+ import type { IAppNav } from "@peers-app/peers-sdk";
2
+ import type { IPackageWithNavs } from "../ui-router/routes-loader";
2
3
  export { accountApp } from "./account.app";
3
4
  export { assistantsApp } from "./assistants.app";
4
5
  export { consoleLogsApp } from "./console-logs.app";
@@ -17,4 +18,4 @@ export { typesApp } from "./types.app";
17
18
  export { variablesApp } from "./variables.app";
18
19
  export { workflowsApp } from "./workflows.app";
19
20
  export declare const systemApps: IAppNav[];
20
- export declare const systemPackage: IPackage;
21
+ export declare const systemPackage: IPackageWithNavs;
@@ -91,5 +91,6 @@ exports.systemPackage = {
91
91
  appNavs: exports.systemApps,
92
92
  createdBy: "system",
93
93
  disabled: false,
94
+ publishPublicKey: "",
94
95
  signature: "",
95
96
  };
@@ -1,4 +1,5 @@
1
- import { type IAppNav, type IPackage, type Observable } from "@peers-app/peers-sdk";
1
+ import { type IAppNav, type Observable } from "@peers-app/peers-sdk";
2
+ import { type IPackageWithNavs } from "../ui-router/routes-loader";
2
3
  export interface TabState {
3
4
  tabId: string;
4
5
  packageId?: string;
@@ -21,7 +22,7 @@ export declare function goToTabPath(path: string): void;
21
22
  export declare const handleMainPathChanged: (oldPath: string, newPath: string, setNewMainPath: (path: string) => void) => void;
22
23
  type AppInfo = {
23
24
  navItem: IAppNav;
24
- package: IPackage;
25
+ package: IPackageWithNavs;
25
26
  };
26
27
  export declare function determineAppFromPath(path: string): AppInfo | undefined;
27
28
  export declare const updateActiveTabTitle: (newTitle: string) => void;
@@ -1,20 +1,8 @@
1
+ import { type IAppNav, type IPackage } from "@peers-app/peers-sdk";
1
2
  import "../ui-defaults";
2
- export declare const allPackages: import("@peers-app/peers-sdk").Observable<{
3
- name: string;
4
- description: string;
5
- signature: string;
6
- packageId: string;
7
- createdBy: string;
8
- disabled?: boolean | undefined;
9
- appNavs?: {
10
- name: string;
11
- iconClassName: string;
12
- navigationPath: string;
13
- displayName?: string | undefined;
14
- }[] | undefined;
15
- remoteRepo?: string | undefined;
16
- activePackageVersionId?: string | undefined;
17
- versionFollowRange?: "pinned" | "patch" | "minor" | "latest" | undefined;
18
- followVersionTags?: string | undefined;
19
- }[]>;
3
+ /** IPackage enriched with appNavs from the active PackageVersion. */
4
+ export type IPackageWithNavs = IPackage & {
5
+ appNavs?: IAppNav[];
6
+ };
7
+ export declare const allPackages: import("@peers-app/peers-sdk").Observable<IPackageWithNavs[]>;
20
8
  export declare const loadAllRoutes: () => Promise<true | undefined>;
@@ -31,8 +31,7 @@ function subscribeToPrefs(packageId) {
31
31
  });
32
32
  }
33
33
  /**
34
- * Enrich an IPackage with appNavs from the device's active PV record.
35
- * Falls back to `pkg.appNavs` for backward compat with older data.
34
+ * Enrich an IPackage with appNavs from the device's active PackageVersion record.
36
35
  */
37
36
  async function enrichWithPvAppNavs(pkg) {
38
37
  let pvId = pkg.activePackageVersionId;
@@ -53,7 +52,7 @@ async function enrichWithPvAppNavs(pkg) {
53
52
  }
54
53
  }
55
54
  catch {
56
- /* fall through to pkg.appNavs */
55
+ /* PV not found */
57
56
  }
58
57
  return pkg;
59
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.18.8",
3
+ "version": "0.19.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.18.8",
31
+ "@peers-app/peers-sdk": "^0.19.6",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-env": "^7.24.5",
40
40
  "@babel/preset-react": "^7.24.1",
41
41
  "@babel/preset-typescript": "^7.27.1",
42
- "@peers-app/peers-sdk": "0.18.8",
42
+ "@peers-app/peers-sdk": "0.19.6",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
package/src/index.tsx CHANGED
@@ -6,6 +6,7 @@ export * from "./components/markdown-editor/editor-inline";
6
6
  export * from "./components/markdown-with-mentions";
7
7
  export * from "./components/sortable-list";
8
8
  export * from "./components/tabs";
9
+ export { Typeahead, type TypeaheadItem } from "./components/typeahead";
9
10
  export * from "./components/voice-indicator";
10
11
  export { mainContentPath } from "./globals";
11
12
  export * from "./hooks";
@@ -7,7 +7,6 @@ import {
7
7
  newid,
8
8
  newKeys,
9
9
  observable,
10
- peersCorePackageId,
11
10
  rpcServerCalls,
12
11
  setUserTrustLevel,
13
12
  TrustLevel,
@@ -132,11 +131,9 @@ export function GroupList() {
132
131
  await Groups(groupDataContext).signAndSave(group);
133
132
 
134
133
  // Auto-install peers-core for the new group
135
- await rpcServerCalls
136
- .addOrUpdatePackage(peersCorePackageId, { dataContextId: groupId })
137
- .catch((err: unknown) => {
138
- console.error("Error auto-installing peers-core for group:", err);
139
- });
134
+ await rpcServerCalls.seedBundledPeersCore(groupId).catch((err: unknown) => {
135
+ console.error("Error auto-installing peers-core for group:", err);
136
+ });
140
137
 
141
138
  mainContentPath(`groups/${group.groupId}`);
142
139
  return group;
@@ -1,5 +1,4 @@
1
1
  import { Packages, rpcServerCalls } from "@peers-app/peers-sdk";
2
- import React from "react";
3
2
  import { Input } from "../../components/input";
4
3
  import { LoadingIndicator } from "../../components/loading-indicator";
5
4
  import { type ISaveButtonProps, SaveButton } from "../../components/save-button";
@@ -13,9 +12,9 @@ interface IProps {
13
12
  packageId: string;
14
13
  }
15
14
 
15
+ /** Detail screen for a single package with Info, Versions, Components, and Dependencies tabs. */
16
16
  export const PackageDetails = (props: IProps) => {
17
17
  const refresh = useObservableState(Date.now());
18
- const saveDeviceTagRef = React.useRef<(() => Promise<void>) | null>(null);
19
18
 
20
19
  const pkg = usePromise(
21
20
  async () => {
@@ -88,7 +87,6 @@ export const PackageDetails = (props: IProps) => {
88
87
  doc={pkg}
89
88
  onClick={async () => {
90
89
  await pkg.save();
91
- await saveDeviceTagRef.current?.();
92
90
  }}
93
91
  addActions={[...addActions]}
94
92
  />
@@ -102,7 +100,7 @@ export const PackageDetails = (props: IProps) => {
102
100
  name: "Info",
103
101
  content: (
104
102
  <ScreenTabBody>
105
- <PackageInfo pkg={pkg} saveDeviceTagRef={saveDeviceTagRef} />
103
+ <PackageInfo pkg={pkg} />
106
104
  </ScreenTabBody>
107
105
  ),
108
106
  },
@@ -0,0 +1,140 @@
1
+ import {
2
+ doesTagMatch,
3
+ GroupMemberRole,
4
+ getUserContext,
5
+ getUserRole,
6
+ hasDeviceFollowOverride,
7
+ type IPackageVersion,
8
+ Packages,
9
+ PackageVersions,
10
+ packagePrefsVar,
11
+ updatePackagePrefs,
12
+ } from "@peers-app/peers-sdk";
13
+
14
+ /** Severity of a version update: major > minor > patch. */
15
+ export type UpdateLevel = "major" | "minor" | "patch" | null;
16
+
17
+ /** Result of checking a package's version status against available versions. */
18
+ export interface IVersionStatus {
19
+ activePv: IPackageVersion;
20
+ newerLevel: UpdateLevel;
21
+ newestPv: IPackageVersion | null;
22
+ }
23
+
24
+ /**
25
+ * Check a package's version status: what's currently active, whether a newer
26
+ * version exists within the follow policy, and which version is the best
27
+ * upgrade candidate.
28
+ *
29
+ * @param packageId - The package to check.
30
+ * @param activePackageVersionId - The effective active version (device or group).
31
+ * @param followVersionTags - Tag filter from effective prefs.
32
+ */
33
+ export async function checkVersionStatus(
34
+ packageId: string,
35
+ activePackageVersionId: string,
36
+ followVersionTags: string | undefined,
37
+ ): Promise<IVersionStatus | null> {
38
+ const all = await PackageVersions().list({ packageId });
39
+ const active = all.find((v) => v.packageVersionId === activePackageVersionId);
40
+ if (!active) return null;
41
+
42
+ let newerLevel: UpdateLevel = null;
43
+ let newestPv: IPackageVersion | null = null;
44
+
45
+ if (active.version) {
46
+ const parse = (v: string) => v.split(".").map(Number);
47
+ const [aMaj, aMin, aPat] = parse(active.version);
48
+ let bestVersion = active.version;
49
+
50
+ for (const v of all) {
51
+ if (!v.version || v.packageVersionId === activePackageVersionId) continue;
52
+ if (!doesTagMatch(active.versionTag, v.versionTag, followVersionTags, undefined)) continue;
53
+ const [maj, min, pat] = parse(v.version);
54
+ const [bMaj, bMin, bPat] = parse(bestVersion);
55
+
56
+ let candidateLevel: UpdateLevel = null;
57
+ if (maj > aMaj) candidateLevel = "major";
58
+ else if (maj === aMaj && min > aMin) candidateLevel = "minor";
59
+ else if (maj === aMaj && min === aMin && pat > aPat) candidateLevel = "patch";
60
+ else if (
61
+ maj === aMaj &&
62
+ min === aMin &&
63
+ pat === aPat &&
64
+ (v.createdAt || "") > (active.createdAt || "")
65
+ )
66
+ candidateLevel = "patch";
67
+
68
+ if (!candidateLevel) continue;
69
+
70
+ const levelRank = { major: 3, minor: 2, patch: 1 };
71
+ if (!newerLevel || levelRank[candidateLevel] > levelRank[newerLevel]) {
72
+ newerLevel = candidateLevel;
73
+ }
74
+
75
+ const isHigherThanBest =
76
+ maj > bMaj ||
77
+ (maj === bMaj && min > bMin) ||
78
+ (maj === bMaj && min === bMin && pat > bPat) ||
79
+ (maj === bMaj &&
80
+ min === bMin &&
81
+ pat === bPat &&
82
+ (v.createdAt || "") > (newestPv?.createdAt || active.createdAt || ""));
83
+
84
+ if (!newestPv || isHigherThanBest) {
85
+ newestPv = v;
86
+ bestVersion = v.version;
87
+ }
88
+ }
89
+ }
90
+ return { activePv: active, newerLevel, newestPv };
91
+ }
92
+
93
+ /** Whether the current user is an admin of the active group. */
94
+ export async function isGroupAdmin(): Promise<boolean> {
95
+ try {
96
+ const userCtx = await getUserContext();
97
+ const dc = userCtx.defaultDataContext();
98
+ const contextId = dc?.groupId || dc?.dataContextId;
99
+ if (!contextId) return false;
100
+ const role = await getUserRole(contextId, userCtx.userId);
101
+ return role >= GroupMemberRole.Admin;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Activate a specific package version, handling group-level vs device-local
109
+ * activation. When the device has no follow override and the user is a group
110
+ * admin, the group's `activePackageVersionId` is advanced so all devices
111
+ * stay in sync. Otherwise only the device-local prefs are updated.
112
+ *
113
+ * @param packageId - The package to activate a version for.
114
+ * @param pv - The package version to activate.
115
+ */
116
+ export async function activatePackageVersion(
117
+ packageId: string,
118
+ pv: IPackageVersion,
119
+ ): Promise<void> {
120
+ const prefsVar = packagePrefsVar(packageId);
121
+ await prefsVar.loadingPromise;
122
+ const devicePrefs = prefsVar();
123
+
124
+ const activatesGroup =
125
+ pv.versionTag !== "dev" && !hasDeviceFollowOverride(devicePrefs) && (await isGroupAdmin());
126
+
127
+ if (activatesGroup) {
128
+ const current = await Packages().get(packageId);
129
+ if (current) {
130
+ current.activePackageVersionId = pv.packageVersionId;
131
+ await Packages().signAndSave(current);
132
+ }
133
+ await updatePackagePrefs(packageId, { activePackageVersionId: undefined });
134
+ } else {
135
+ await updatePackagePrefs(packageId, { activePackageVersionId: pv.packageVersionId });
136
+ }
137
+ }
138
+
139
+ /** Numeric rank for sorting packages by update urgency. */
140
+ export const updateLevelRank: Record<string, number> = { major: 3, minor: 2, patch: 1 };