@peers-app/peers-ui 0.26.4 → 0.26.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.
Files changed (59) hide show
  1. package/dist/components/group-switcher.d.ts +6 -0
  2. package/dist/components/group-switcher.js +8 -1
  3. package/dist/components/markdown-with-mentions.d.ts +4 -0
  4. package/dist/components/markdown-with-mentions.js +6 -1
  5. package/dist/components/save-button.d.ts +15 -0
  6. package/dist/components/save-button.js +14 -3
  7. package/dist/globals.js +34 -8
  8. package/dist/host-shell/host-shell-consumer.d.ts +21 -0
  9. package/dist/host-shell/host-shell-consumer.js +50 -0
  10. package/dist/host-shell/host-shell-consumer.test.d.ts +1 -0
  11. package/dist/host-shell/host-shell-consumer.test.js +28 -0
  12. package/dist/mention-configs.js +2 -1
  13. package/dist/screens/groups/group-list.js +4 -1
  14. package/dist/screens/packages/package-details.d.ts +1 -0
  15. package/dist/screens/packages/package-details.js +27 -8
  16. package/dist/screens/packages/package-helpers.d.ts +3 -23
  17. package/dist/screens/packages/package-helpers.js +12 -46
  18. package/dist/screens/packages/package-helpers.test.d.ts +1 -0
  19. package/dist/screens/packages/package-helpers.test.js +53 -0
  20. package/dist/screens/packages/package-info.js +3 -2
  21. package/dist/screens/packages/package-list.d.ts +4 -2
  22. package/dist/screens/packages/package-list.js +50 -10
  23. package/dist/screens/packages/package-new-local.js +7 -4
  24. package/dist/screens/packages/package-runtime-status.d.ts +12 -0
  25. package/dist/screens/packages/package-runtime-status.js +33 -0
  26. package/dist/screens/packages/package-runtime-status.test.d.ts +1 -0
  27. package/dist/screens/packages/package-runtime-status.test.js +50 -0
  28. package/dist/screens/packages/package-versions.js +8 -55
  29. package/dist/screens/packages/packages-ipc.d.ts +41 -0
  30. package/dist/screens/packages/packages-ipc.js +102 -0
  31. package/dist/screens/packages/packages-ipc.test.d.ts +1 -0
  32. package/dist/screens/packages/packages-ipc.test.js +43 -0
  33. package/dist/screens/tools/tool-catalog-ipc.test.js +6 -0
  34. package/dist/screens/variables/variable-details.d.ts +1 -0
  35. package/dist/screens/variables/variable-details.js +11 -5
  36. package/dist/ui-router/ui-loader.js +10 -37
  37. package/package.json +3 -3
  38. package/src/components/group-switcher.tsx +12 -4
  39. package/src/components/markdown-with-mentions.tsx +7 -2
  40. package/src/components/save-button.tsx +24 -3
  41. package/src/globals.tsx +32 -11
  42. package/src/host-shell/host-shell-consumer.test.ts +32 -0
  43. package/src/host-shell/host-shell-consumer.ts +53 -0
  44. package/src/mention-configs.ts +2 -2
  45. package/src/screens/groups/group-list.tsx +6 -4
  46. package/src/screens/packages/package-details.tsx +31 -8
  47. package/src/screens/packages/package-helpers.test.ts +57 -0
  48. package/src/screens/packages/package-helpers.ts +13 -55
  49. package/src/screens/packages/package-info.tsx +3 -3
  50. package/src/screens/packages/package-list.tsx +51 -16
  51. package/src/screens/packages/package-new-local.tsx +8 -13
  52. package/src/screens/packages/package-runtime-status.test.tsx +58 -0
  53. package/src/screens/packages/package-runtime-status.tsx +68 -0
  54. package/src/screens/packages/package-versions.tsx +9 -72
  55. package/src/screens/packages/packages-ipc.test.ts +47 -0
  56. package/src/screens/packages/packages-ipc.ts +125 -0
  57. package/src/screens/tools/tool-catalog-ipc.test.ts +7 -1
  58. package/src/screens/variables/variable-details.tsx +11 -6
  59. package/src/ui-router/ui-loader.tsx +12 -51
@@ -1,6 +1,12 @@
1
1
  interface GroupSwitcherProps {
2
+ /** Current theme (`light` or `dark`). */
2
3
  colorMode: string;
4
+ /** Use the compact mobile layout. */
3
5
  isMobile?: boolean;
4
6
  }
7
+ /**
8
+ * Header dropdown for switching the active group (or the personal context) and creating a
9
+ * new group, which also installs the default packages into it.
10
+ */
5
11
  export declare function GroupSwitcher({ colorMode, isMobile }: GroupSwitcherProps): import("react/jsx-runtime").JSX.Element;
6
12
  export {};
@@ -38,6 +38,11 @@ const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const peers_sdk_1 = require("@peers-app/peers-sdk");
39
39
  const react_1 = __importStar(require("react"));
40
40
  const hooks_1 = require("../hooks");
41
+ const packages_ipc_1 = require("../screens/packages/packages-ipc");
42
+ /**
43
+ * Header dropdown for switching the active group (or the personal context) and creating a
44
+ * new group, which also installs the default packages into it.
45
+ */
41
46
  function GroupSwitcher({ colorMode, isMobile = false }) {
42
47
  const [showDropdown, setShowDropdown] = (0, react_1.useState)(false);
43
48
  const [showCreateModal, setShowCreateModal] = (0, react_1.useState)(false);
@@ -183,7 +188,9 @@ function CreateGroupModal({ colorMode, userContext, onClose, onGroupCreated, })
183
188
  await (0, peers_sdk_1.Groups)(groupDataContext).signAndSave(newGroup);
184
189
  // Auto-install peers-core for the new group (copy from another context that
185
190
  // already has it, else fall back to the remote updateUrl / bundle).
186
- await peers_sdk_1.rpcServerCalls.seedBundledPeersCore(groupId).catch((err) => {
191
+ await (0, packages_ipc_1.packagesTools)(groupId)
192
+ .then((tools) => tools.installDefaultPackages())
193
+ .catch((err) => {
187
194
  console.error("Error auto-installing peers-core for group:", err);
188
195
  });
189
196
  // Notify parent
@@ -1,4 +1,8 @@
1
1
  import React from "react";
2
+ /**
3
+ * Render markdown (GitHub-flavored) with `@mentions` turned into links. Web links open in
4
+ * the system browser through System Host Shell instead of navigating the app window.
5
+ */
2
6
  export declare const MarkdownWithMentions: React.FC<{
3
7
  content: string;
4
8
  }>;
@@ -10,6 +10,7 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
10
10
  const react_1 = __importDefault(require("react"));
11
11
  const react_markdown_1 = __importDefault(require("react-markdown"));
12
12
  const remark_gfm_1 = __importDefault(require("remark-gfm"));
13
+ const host_shell_consumer_1 = require("../host-shell/host-shell-consumer");
13
14
  const mention_configs_1 = require("../mention-configs");
14
15
  // Custom Link Renderer
15
16
  const LinkRenderer = (props) => {
@@ -17,7 +18,7 @@ const LinkRenderer = (props) => {
17
18
  return ((0, jsx_runtime_1.jsx)("a", { href: href, onClick: (e) => {
18
19
  if (href?.startsWith("http")) {
19
20
  e.preventDefault();
20
- peers_sdk_1.rpcServerCalls.openLinkInBrowser(href);
21
+ void (0, host_shell_consumer_1.openOnHost)(href);
21
22
  }
22
23
  }, children: props.children }));
23
24
  };
@@ -114,6 +115,10 @@ const createElementWithMentions = (Element) => {
114
115
  }) }));
115
116
  };
116
117
  };
118
+ /**
119
+ * Render markdown (GitHub-flavored) with `@mentions` turned into links. Web links open in
120
+ * the system browser through System Host Shell instead of navigating the app window.
121
+ */
117
122
  const MarkdownWithMentions = ({ content }) => {
118
123
  const components = {
119
124
  p: createElementWithMentions("p"),
@@ -1,14 +1,29 @@
1
1
  import type { IDoc } from "@peers-app/peers-sdk";
2
2
  import type React from "react";
3
+ /** Props for {@link SaveButton}. */
3
4
  export interface ISaveButtonProps {
4
5
  /** Used with many concrete table row shapes; IDoc stays generic at call sites. */
5
6
  doc: IDoc<any>;
7
+ /** Save handler; defaults to `doc.save`. */
6
8
  onClick?: () => void | Promise<void>;
9
+ /** Extra dropdown actions: buttons, or arbitrary nodes such as dividers. */
7
10
  addActions?: ({
8
11
  label: string;
9
12
  onClick: () => void;
10
13
  className?: string;
11
14
  } | React.ReactNode)[];
15
+ /** Hide the built-in Discard Changes and Delete actions. */
12
16
  noBuiltInActions?: boolean;
17
+ /** Label for the built-in delete action. Default: `Delete`. */
18
+ deleteLabel?: string;
19
+ /**
20
+ * Replace the built-in delete (`doc.delete()`), for records whose removal must go through
21
+ * a Runtime contract. Runs after the user confirms; the screen then navigates up.
22
+ */
23
+ onDelete?: () => void | Promise<void>;
13
24
  }
25
+ /**
26
+ * Save button with a dropdown of extra actions plus built-in Discard Changes and Delete.
27
+ * Enabled only while the document has unsaved changes.
28
+ */
14
29
  export declare const SaveButton: (props: ISaveButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -4,6 +4,10 @@ exports.SaveButton = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const globals_1 = require("../globals");
6
6
  const hooks_1 = require("../hooks");
7
+ /**
8
+ * Save button with a dropdown of extra actions plus built-in Discard Changes and Delete.
9
+ * Enabled only while the document has unsaved changes.
10
+ */
7
11
  const SaveButton = (props) => {
8
12
  let { doc, addActions } = props;
9
13
  (0, hooks_1.useObservable)(doc.q);
@@ -29,12 +33,19 @@ const SaveButton = (props) => {
29
33
  if (allActions.length) {
30
34
  allActions.push((0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider mt-3" }));
31
35
  }
36
+ const deleteLabel = props.deleteLabel ?? "Delete";
32
37
  allActions.push({
33
- label: "Delete",
38
+ label: deleteLabel,
34
39
  className: `text-bg-danger`,
35
40
  onClick: async () => {
36
- if (confirm("Are you sure you want to delete this?")) {
37
- await doc.delete();
41
+ if (confirm(`Are you sure you want to ${deleteLabel.toLowerCase()} this?`)) {
42
+ try {
43
+ await (props.onDelete ? props.onDelete() : doc.delete());
44
+ }
45
+ catch (err) {
46
+ alert(`${deleteLabel} failed: ${err instanceof Error ? err.message : String(err)}`);
47
+ return;
48
+ }
38
49
  const pathParts = (0, globals_1.mainContentPath)().split("/");
39
50
  pathParts.pop();
40
51
  const path = pathParts.join("/") ?? "";
package/dist/globals.js CHANGED
@@ -8,6 +8,7 @@ exports.initializeHashNavigation = initializeHashNavigation;
8
8
  exports.loadGlobals = loadGlobals;
9
9
  const peers_sdk_1 = require("@peers-app/peers-sdk");
10
10
  const identity_routes_1 = require("./screens/identity/identity-routes");
11
+ const packages_ipc_1 = require("./screens/packages/packages-ipc");
11
12
  const tabs_state_1 = require("./tabs-layout/tabs-state");
12
13
  const utils_1 = require("./utils");
13
14
  /** Counter incremented when installed package UI should reload. */
@@ -90,13 +91,11 @@ async function openThreadInTab(thread) {
90
91
  iconClassName: "bi-chat-dots",
91
92
  });
92
93
  }
93
- peers_sdk_1.rpcClientCalls.setClientPath = async (url) => {
94
- (0, exports.mainContentPath)((url && String(url).trim()) || "");
95
- };
96
- peers_sdk_1.rpcClientCalls.openThread = async (threadId) => {
97
- console.warn("Opening thread:", threadId);
98
- openThread(threadId);
99
- };
94
+ // A notification clicked in the desktop app's main process opens its thread here.
95
+ (0, peers_sdk_1.subscribe)("ui:openThread", (event) => {
96
+ if (event.data?.threadId)
97
+ void openThread(event.data.threadId);
98
+ });
100
99
  /** Signed profile for the active user after {@link loadGlobals}. */
101
100
  exports.me = null;
102
101
  /** Current browser viewport width. */
@@ -153,6 +152,33 @@ function initializeHashNavigation() {
153
152
  updateWindowHash();
154
153
  }
155
154
  }
155
+ /** Longest page startup waits for package folders to be re-read. */
156
+ const PACKAGE_FOLDER_RELOAD_WAIT_MS = 30_000;
157
+ /**
158
+ * Re-read this device's package folders so rebuilt dev bundles load before the UI does.
159
+ * Skipped on hosts without package folders; failures are logged, and the wait is bounded so
160
+ * a slow host cannot stall startup.
161
+ */
162
+ async function reloadPackageFolders() {
163
+ const reload = (async () => {
164
+ const tools = await (0, packages_ipc_1.packagesTools)();
165
+ const { capabilities } = await tools.getCapabilities();
166
+ if (!capabilities.fileSystem)
167
+ return;
168
+ const { skipped, results } = await tools.reloadAllPackages();
169
+ if (skipped)
170
+ console.warn(skipped);
171
+ for (const result of results) {
172
+ if (!result.ok)
173
+ console.warn(`[packages] Reloading ${result.name} failed: ${result.error}`);
174
+ if (result.warning)
175
+ console.warn(`[packages] ${result.name}: ${result.warning}`);
176
+ }
177
+ })().catch((err) => {
178
+ console.warn("[packages] Reloading package folders failed:", err);
179
+ });
180
+ await Promise.race([reload, (0, peers_sdk_1.sleep)(PACKAGE_FOLDER_RELOAD_WAIT_MS)]);
181
+ }
156
182
  /** Load global user state required before the root layout renders. */
157
183
  async function loadGlobals() {
158
184
  if (globalsLoaded)
@@ -161,7 +187,7 @@ async function loadGlobals() {
161
187
  exports.me = await (0, peers_sdk_1.getMe)();
162
188
  await peers_sdk_1.reloadPackagesOnPageRefresh.loadingPromise;
163
189
  if ((0, peers_sdk_1.reloadPackagesOnPageRefresh)()) {
164
- await peers_sdk_1.rpcServerCalls.addOrUpdatePackage("updateAll");
190
+ await reloadPackageFolders();
165
191
  }
166
192
  await Promise.all([
167
193
  (0, peers_sdk_1.Groups)()
@@ -0,0 +1,21 @@
1
+ import { type IHostShellConsumer } from "@peers-app/peers-sdk";
2
+ /**
3
+ * Shared handle for System Host Shell on the local host. Calls name no data context, so the
4
+ * host runs them in the personal context, where the signed-in user has Self access.
5
+ */
6
+ export declare const hostShellContract: import("@peers-app/peers-sdk").IHostContractConsumer<IHostShellConsumer>;
7
+ /** Return the System Host Shell tools of the local host. */
8
+ export declare function hostShellTools(): Promise<IHostShellConsumer["tools"]>;
9
+ /**
10
+ * Open a web link in the browser, or a local file or folder with the OS. Failures (an
11
+ * unsupported host, a refused link scheme, a missing folder) are logged and shown.
12
+ *
13
+ * @param target - Link or local path (`~` allowed).
14
+ */
15
+ export declare function openOnHost(target: string): Promise<void>;
16
+ /**
17
+ * Open a folder in a code editor (VS Code when installed), else with the OS.
18
+ *
19
+ * @param path - Folder or file (`~` allowed).
20
+ */
21
+ export declare function openInEditorOnHost(path: string): Promise<void>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hostShellContract = void 0;
4
+ exports.hostShellTools = hostShellTools;
5
+ exports.openOnHost = openOnHost;
6
+ exports.openInEditorOnHost = openInEditorOnHost;
7
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
8
+ /**
9
+ * Shared handle for System Host Shell on the local host. Calls name no data context, so the
10
+ * host runs them in the personal context, where the signed-in user has Self access.
11
+ */
12
+ exports.hostShellContract = (0, peers_sdk_1.hostContractConsumer)((0, peers_sdk_1.getHostShellContractDefinition)());
13
+ /** Return the System Host Shell tools of the local host. */
14
+ async function hostShellTools() {
15
+ return (await exports.hostShellContract.get()).tools;
16
+ }
17
+ /**
18
+ * Open a web link in the browser, or a local file or folder with the OS. Failures (an
19
+ * unsupported host, a refused link scheme, a missing folder) are logged and shown.
20
+ *
21
+ * @param target - Link or local path (`~` allowed).
22
+ */
23
+ async function openOnHost(target) {
24
+ try {
25
+ await (await hostShellTools()).open({ target });
26
+ }
27
+ catch (error) {
28
+ reportHostShellError(`Could not open ${target}`, error);
29
+ }
30
+ }
31
+ /**
32
+ * Open a folder in a code editor (VS Code when installed), else with the OS.
33
+ *
34
+ * @param path - Folder or file (`~` allowed).
35
+ */
36
+ async function openInEditorOnHost(path) {
37
+ try {
38
+ await (await hostShellTools()).openInEditor({ path });
39
+ }
40
+ catch (error) {
41
+ reportHostShellError(`Could not open ${path}`, error);
42
+ }
43
+ }
44
+ function reportHostShellError(what, error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ console.error(`${what}:`, error);
47
+ if (typeof window !== "undefined" && typeof window.alert === "function") {
48
+ window.alert(`${what}: ${message}`);
49
+ }
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockTools = { open: jest.fn(), openInEditor: jest.fn() };
4
+ jest.mock("@peers-app/peers-sdk", () => ({
5
+ getHostShellContractDefinition: () => ({ contractId: "host-shell" }),
6
+ hostContractConsumer: () => ({ get: async () => ({ tools: mockTools }), dispose: () => { } }),
7
+ }));
8
+ const host_shell_consumer_1 = require("./host-shell-consumer");
9
+ const alert = jest.fn();
10
+ beforeEach(() => {
11
+ Object.assign(window, { alert });
12
+ jest.spyOn(console, "error").mockImplementation(() => { });
13
+ mockTools.open.mockReset();
14
+ mockTools.openInEditor.mockReset();
15
+ alert.mockReset();
16
+ });
17
+ it("opens links and folders on the host", async () => {
18
+ await (0, host_shell_consumer_1.openOnHost)("https://peers.app");
19
+ expect(mockTools.open).toHaveBeenCalledWith({ target: "https://peers.app" });
20
+ await (0, host_shell_consumer_1.openInEditorOnHost)("~/peers/packages/desk");
21
+ expect(mockTools.openInEditor).toHaveBeenCalledWith({ path: "~/peers/packages/desk" });
22
+ expect(alert).not.toHaveBeenCalled();
23
+ });
24
+ it("shows why the host could not open something", async () => {
25
+ mockTools.open.mockRejectedValueOnce(new Error("open refuses git: links"));
26
+ await (0, host_shell_consumer_1.openOnHost)("git@github.com:me/pkg.git");
27
+ expect(alert).toHaveBeenCalledWith("Could not open git@github.com:me/pkg.git: open refuses git: links");
28
+ });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mentionConfigs = exports.valueTypeMentionConfig = exports.workflowMentionConfig = exports.assistantMentionConfig = exports.userMentionConfig = exports.toolMentionConfig = void 0;
4
4
  const peers_sdk_1 = require("@peers-app/peers-sdk");
5
+ const globals_1 = require("./globals");
5
6
  const tool_catalog_ipc_1 = require("./screens/tools/tool-catalog-ipc");
6
7
  const MENTIONS_MAX_RESULTS = 10;
7
8
  /** Discover workflow-invocable tools while keeping historical mention IDs readable. */
@@ -111,7 +112,7 @@ exports.valueTypeMentionConfig = {
111
112
  name: item.name,
112
113
  }),
113
114
  onClick(data) {
114
- peers_sdk_1.rpcClientCalls.setClientPath(`peer-types/${data.id}`);
115
+ (0, globals_1.mainContentPath)(`peer-types/${data.id}`);
115
116
  },
116
117
  };
117
118
  // Tasks().list({ title: { $matchWords: mentionString }, completeDT: { $exists: searchCompletedTasks } }, { pageSize: MAX_RESULTS })
@@ -10,6 +10,7 @@ const globals_1 = require("../../globals");
10
10
  const hooks_1 = require("../../hooks");
11
11
  const identity_routes_1 = require("../identity/identity-routes");
12
12
  const activity_view_1 = require("../invites/activity-view");
13
+ const packages_ipc_1 = require("../packages/packages-ipc");
13
14
  /** Search, summarize, and create groups owned by the signed-in user. */
14
15
  function GroupList(props = {}) {
15
16
  const userContext = (0, hooks_1.usePromise)(peers_sdk_1.getUserContext);
@@ -122,7 +123,9 @@ function GroupList(props = {}) {
122
123
  });
123
124
  await (0, peers_sdk_1.Groups)(groupDataContext).signAndSave(group);
124
125
  // Auto-install peers-core for the new group
125
- await peers_sdk_1.rpcServerCalls.seedBundledPeersCore(groupId).catch((err) => {
126
+ await (0, packages_ipc_1.packagesTools)(groupId)
127
+ .then((tools) => tools.installDefaultPackages())
128
+ .catch((err) => {
126
129
  console.error("Error auto-installing peers-core for group:", err);
127
130
  });
128
131
  (0, globals_1.mainContentPath)(`${identity_routes_1.IDENTITY_GROUPS_PATH}/${group.groupId}`);
@@ -1,4 +1,5 @@
1
1
  interface IProps {
2
+ /** Package to show. */
2
3
  packageId: string;
3
4
  }
4
5
  /** Detail screen for a single package with Info, Versions, Components, and Dependencies tabs. */
@@ -11,7 +11,9 @@ const hooks_1 = require("../../hooks");
11
11
  const tabs_state_1 = require("../../tabs-layout/tabs-state");
12
12
  const local_folder_grants_panel_1 = require("./local-folder-grants-panel");
13
13
  const package_info_1 = require("./package-info");
14
+ const package_runtime_status_1 = require("./package-runtime-status");
14
15
  const package_versions_1 = require("./package-versions");
16
+ const packages_ipc_1 = require("./packages-ipc");
15
17
  /** Detail screen for a single package with Info, Versions, Components, and Dependencies tabs. */
16
18
  const PackageDetails = (props) => {
17
19
  const refresh = (0, hooks_1.useObservableState)(Date.now());
@@ -30,7 +32,17 @@ const PackageDetails = (props) => {
30
32
  {
31
33
  label: "Reload Package",
32
34
  onClick: async () => {
33
- await peers_sdk_1.rpcServerCalls.addOrUpdatePackage(pkg.packageId, { update: true });
35
+ try {
36
+ const { warning } = await (await (0, packages_ipc_1.packagesTools)()).reloadPackage({
37
+ packageId: pkg.packageId,
38
+ });
39
+ if (warning)
40
+ alert(warning);
41
+ }
42
+ catch (err) {
43
+ alert(`Reload failed: ${err instanceof Error ? err.message : String(err)}`);
44
+ return;
45
+ }
34
46
  await pkg.load();
35
47
  window.location.reload();
36
48
  },
@@ -42,16 +54,21 @@ const PackageDetails = (props) => {
42
54
  // className: 'text-bg-warning',
43
55
  onClick: async () => {
44
56
  if (confirm(`Create a remote repository from this local one: ${pkg.name}`)) {
57
+ const tools = await (0, packages_ipc_1.packagesTools)();
58
+ try {
59
+ await tools.linkRemoteRepo({ packageId: pkg.packageId });
60
+ }
61
+ catch (err) {
62
+ alert(`Error linking remote repo: ${err instanceof Error ? err.message : err}`);
63
+ return;
64
+ }
45
65
  try {
46
- await peers_sdk_1.rpcServerCalls.addOrUpdatePackage(pkg.packageId, {
47
- linkRemote: true,
48
- update: true,
49
- });
50
- refresh(Date.now());
66
+ await tools.reloadPackage({ packageId: pkg.packageId });
51
67
  }
52
68
  catch (err) {
53
- alert(`Error linking remote repo: ${err}`);
69
+ alert(`Linked the remote repo, but reloading failed: ${err instanceof Error ? err.message : err}`);
54
70
  }
71
+ refresh(Date.now());
55
72
  }
56
73
  },
57
74
  });
@@ -59,7 +76,9 @@ const PackageDetails = (props) => {
59
76
  return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex", children: [(0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("h4", { children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-fill me-2" }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1", children: (0, jsx_runtime_1.jsx)("h4", { children: (0, jsx_runtime_1.jsx)(input_1.Input, { className: "border border-0", style: { width: "100%", outline: "none", backgroundColor: "transparent" }, value: pkg.qs.name }, pkg.packageId) }) }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(save_button_1.SaveButton, { doc: pkg, onClick: async () => {
60
77
  await (0, peers_sdk_1.Packages)().signAndSave(pkg.toJS());
61
78
  pkg.q(0);
62
- }, addActions: [...addActions] }, pkg.packageId) })] }), (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: [
79
+ }, addActions: [...addActions], deleteLabel: "Uninstall", onDelete: async () => {
80
+ await (await (0, packages_ipc_1.packagesTools)()).uninstallPackage({ packageId: pkg.packageId });
81
+ } }, pkg.packageId) })] }), (0, jsx_runtime_1.jsx)(package_runtime_status_1.PackageRuntimeStatus, { packageId: pkg.packageId }), (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: [
63
82
  {
64
83
  name: "Info",
65
84
  content: ((0, jsx_runtime_1.jsx)(tabs_1.ScreenTabBody, { children: (0, jsx_runtime_1.jsx)(package_info_1.PackageInfo, { pkg: pkg }) })),
@@ -22,33 +22,13 @@ export declare function isGroupAdmin(): Promise<boolean>;
22
22
  /**
23
23
  * Activate a specific package version, handling group-level vs device-local
24
24
  * activation. When the device has no follow override and the user is a group
25
- * admin, the group's `activePackageVersionId` is advanced so all devices
26
- * stay in sync. Otherwise only the device-local prefs are updated.
25
+ * admin, a release advances the group's `activePackageVersionId` so all devices
26
+ * stay in sync. Dev versions (including legacy missing or empty tags) and other
27
+ * selections only update the device-local prefs.
27
28
  *
28
29
  * @param packageId - The package to activate a version for.
29
30
  * @param pv - The package version to activate.
30
31
  */
31
32
  export declare function activatePackageVersion(packageId: string, pv: IPackageVersion): Promise<void>;
32
- /**
33
- * After promoting a version that is currently effective-active on this device,
34
- * activate it when the new tag matches the group's auto-activate / follow rules.
35
- *
36
- * This closes the gotcha where a device is already running a `dev` build, promotes
37
- * it to `beta`/`stable`, and expects the group active version to advance without a
38
- * separate Activate click — but only when the group would accept that channel
39
- * (e.g. default follow is `stable`; `beta` only when the group follows beta).
40
- *
41
- * @param packageId - Package that was promoted
42
- * @param promotedPv - PV after the tag change (must include the new `versionTag`)
43
- * @param opts.wasEffectiveActive - True when this PV was this device's effective active before promote
44
- * @param opts.groupFollowTags - Group `IPackage.followVersionTags` (not device override)
45
- * @param opts.groupPinned - True when group `versionFollowRange` is `pinned`
46
- * @returns Whether activation was attempted
47
- */
48
- export declare function maybeActivateAfterPromote(packageId: string, promotedPv: IPackageVersion, opts: {
49
- wasEffectiveActive: boolean;
50
- groupFollowTags: string | undefined;
51
- groupPinned: boolean;
52
- }): Promise<boolean>;
53
33
  /** Numeric rank for sorting packages by update urgency. */
54
34
  export declare const updateLevelRank: Record<string, number>;
@@ -4,8 +4,8 @@ exports.updateLevelRank = void 0;
4
4
  exports.checkVersionStatus = checkVersionStatus;
5
5
  exports.isGroupAdmin = isGroupAdmin;
6
6
  exports.activatePackageVersion = activatePackageVersion;
7
- exports.maybeActivateAfterPromote = maybeActivateAfterPromote;
8
7
  const peers_sdk_1 = require("@peers-app/peers-sdk");
8
+ const packages_ipc_1 = require("./packages-ipc");
9
9
  /**
10
10
  * Check a package's version status: what's currently active, whether a newer
11
11
  * version exists within the follow policy, and which version is the best
@@ -84,8 +84,9 @@ async function isGroupAdmin() {
84
84
  /**
85
85
  * Activate a specific package version, handling group-level vs device-local
86
86
  * activation. When the device has no follow override and the user is a group
87
- * admin, the group's `activePackageVersionId` is advanced so all devices
88
- * stay in sync. Otherwise only the device-local prefs are updated.
87
+ * admin, a release advances the group's `activePackageVersionId` so all devices
88
+ * stay in sync. Dev versions (including legacy missing or empty tags) and other
89
+ * selections only update the device-local prefs.
89
90
  *
90
91
  * @param packageId - The package to activate a version for.
91
92
  * @param pv - The package version to activate.
@@ -94,49 +95,14 @@ async function activatePackageVersion(packageId, pv) {
94
95
  const prefsVar = (0, peers_sdk_1.packagePrefsVar)(packageId);
95
96
  await prefsVar.loadingPromise;
96
97
  const devicePrefs = prefsVar();
97
- const activatesGroup = pv.versionTag !== "dev" && !(0, peers_sdk_1.hasDeviceFollowOverride)(devicePrefs) && (await isGroupAdmin());
98
- if (activatesGroup) {
99
- const current = await (0, peers_sdk_1.Packages)().get(packageId);
100
- if (current) {
101
- current.activePackageVersionId = pv.packageVersionId;
102
- await (0, peers_sdk_1.Packages)().signAndSave(current);
103
- }
104
- await (0, peers_sdk_1.updatePackagePrefs)(packageId, { activePackageVersionId: undefined });
105
- }
106
- else {
107
- await (0, peers_sdk_1.updatePackagePrefs)(packageId, { activePackageVersionId: pv.packageVersionId });
108
- }
109
- }
110
- /**
111
- * After promoting a version that is currently effective-active on this device,
112
- * activate it when the new tag matches the group's auto-activate / follow rules.
113
- *
114
- * This closes the gotcha where a device is already running a `dev` build, promotes
115
- * it to `beta`/`stable`, and expects the group active version to advance without a
116
- * separate Activate click — but only when the group would accept that channel
117
- * (e.g. default follow is `stable`; `beta` only when the group follows beta).
118
- *
119
- * @param packageId - Package that was promoted
120
- * @param promotedPv - PV after the tag change (must include the new `versionTag`)
121
- * @param opts.wasEffectiveActive - True when this PV was this device's effective active before promote
122
- * @param opts.groupFollowTags - Group `IPackage.followVersionTags` (not device override)
123
- * @param opts.groupPinned - True when group `versionFollowRange` is `pinned`
124
- * @returns Whether activation was attempted
125
- */
126
- async function maybeActivateAfterPromote(packageId, promotedPv, opts) {
127
- if (!opts.wasEffectiveActive)
128
- return false;
129
- if (opts.groupPinned)
130
- return false;
131
- if (!promotedPv.versionTag || promotedPv.versionTag === "dev")
132
- return false;
133
- // Docs default: groups follow `stable` when followVersionTags is unset.
134
- const followTags = opts.groupFollowTags ?? "stable";
135
- if (!(0, peers_sdk_1.doesTagMatch)(undefined, promotedPv.versionTag, followTags, undefined)) {
136
- return false;
137
- }
138
- await activatePackageVersion(packageId, promotedPv);
139
- return true;
98
+ const activatesGroup = (pv.versionTag || "dev") !== "dev" &&
99
+ !(0, peers_sdk_1.hasDeviceFollowOverride)(devicePrefs) &&
100
+ (await isGroupAdmin());
101
+ await (await (0, packages_ipc_1.packagesTools)()).activatePackageVersion({
102
+ packageId,
103
+ packageVersionId: pv.packageVersionId,
104
+ scope: activatesGroup ? "group" : "device",
105
+ });
140
106
  }
141
107
  /** Numeric rank for sorting packages by update urgency. */
142
108
  exports.updateLevelRank = { major: 3, minor: 2, patch: 1 };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
4
+ const package_helpers_1 = require("./package-helpers");
5
+ const mockActivate = jest.fn(async () => ({}));
6
+ const mockRole = jest.fn();
7
+ const mockPrefs = jest.fn();
8
+ jest.mock("./packages-ipc", () => ({
9
+ packagesTools: async () => ({ activatePackageVersion: mockActivate }),
10
+ }));
11
+ jest.mock("@peers-app/peers-sdk", () => {
12
+ const actual = jest.requireActual("@peers-app/peers-sdk");
13
+ return {
14
+ ...actual,
15
+ getUserContext: async () => ({
16
+ userId: "user",
17
+ defaultDataContext: () => ({ dataContextId: "group" }),
18
+ }),
19
+ getUserRole: () => mockRole(),
20
+ packagePrefsVar: () => Object.assign(mockPrefs, { loadingPromise: Promise.resolve() }),
21
+ };
22
+ });
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ mockRole.mockResolvedValue(peers_sdk_1.GroupMemberRole.Admin);
26
+ mockPrefs.mockReturnValue({});
27
+ });
28
+ test.each([
29
+ "dev",
30
+ "",
31
+ undefined,
32
+ ])("activates dev tag %p only on this device", async (versionTag) => {
33
+ const packageId = (0, peers_sdk_1.newid)();
34
+ const packageVersionId = (0, peers_sdk_1.newid)();
35
+ await (0, package_helpers_1.activatePackageVersion)(packageId, { packageVersionId, versionTag });
36
+ expect(mockActivate).toHaveBeenCalledWith({ packageId, packageVersionId, scope: "device" });
37
+ });
38
+ test.each(["beta", "stable"])("an admin activates %s for the group", async (versionTag) => {
39
+ const packageId = (0, peers_sdk_1.newid)();
40
+ const packageVersionId = (0, peers_sdk_1.newid)();
41
+ await (0, package_helpers_1.activatePackageVersion)(packageId, { packageVersionId, versionTag });
42
+ expect(mockActivate).toHaveBeenCalledWith({ packageId, packageVersionId, scope: "group" });
43
+ });
44
+ test("a device follow override keeps release activation local", async () => {
45
+ mockPrefs.mockReturnValue({ followTags: "stable,beta" });
46
+ const packageId = (0, peers_sdk_1.newid)();
47
+ const packageVersionId = (0, peers_sdk_1.newid)();
48
+ await (0, package_helpers_1.activatePackageVersion)(packageId, {
49
+ packageVersionId,
50
+ versionTag: "stable",
51
+ });
52
+ expect(mockActivate).toHaveBeenCalledWith({ packageId, packageVersionId, scope: "device" });
53
+ });
@@ -7,6 +7,7 @@ const input_1 = require("../../components/input");
7
7
  const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
8
8
  const tooltip_1 = require("../../components/tooltip");
9
9
  const hooks_1 = require("../../hooks");
10
+ const host_shell_consumer_1 = require("../../host-shell/host-shell-consumer");
10
11
  const package_network_panel_1 = require("./package-network-panel");
11
12
  /** Info tab for the package details screen. */
12
13
  const PackageInfo = (props) => {
@@ -70,9 +71,9 @@ const PackageInfo = (props) => {
70
71
  }, undefined, [activeVersionId, pkg.packageId, effectiveFollowTags]);
71
72
  const remoteRepoUrl = pkg.remoteRepo;
72
73
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("small", { children: "Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.name, className: "form-control mb-3 p-0 ps-2", placeholder: "Package name", title: "Package name", disabled: true }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Local Path:", (0, jsx_runtime_1.jsxs)("small", { children: [(0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `The local path to the directory containing the package. This will open in VS Code if it's installed otherwise this will open in your native file system` }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-link", onClick: () => {
73
- peers_sdk_1.rpcServerCalls.openPackage(localPath || peers_sdk_1.packagesRootDir);
74
+ void (0, host_shell_consumer_1.openInEditorOnHost)(localPath || peers_sdk_1.packagesRootDir);
74
75
  }, children: "Open Local" })] })] }), (0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control mb-3 p-0 ps-2", placeholder: "~/peers/packages/my-package", value: localPath || "", onChange: (e) => setLocalPath(e.target.value) })] }), remoteRepoUrl ? ((0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Source URL:", (0, jsx_runtime_1.jsxs)("small", { children: [(0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `The remote source of this package. Often a github repository URL or a link to the Peers page for the package.` }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-link", onClick: () => {
75
- peers_sdk_1.rpcServerCalls.openLinkInBrowser(remoteRepoUrl);
76
+ void (0, host_shell_consumer_1.openOnHost)(remoteRepoUrl);
76
77
  }, children: "Open Remote" })] })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.remoteRepo, className: "form-control mb-3 p-0 ps-2", disabled: true })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Update URL:", (0, jsx_runtime_1.jsx)("small", { children: (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `Base URL for remote package updates. Admin devices poll **<updateUrl>/latest-<tag>.json** on startup (for example **latest-stable.json**). Signed artifacts must be published to this host separately — for example via the publish-package tool or the peers-core full-release pipeline.` }) })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: pkg.qs.updateUrl, className: "form-control mb-3 p-0 ps-2", placeholder: "https://example.com/my-package", title: "Update URL" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2", children: [(0, jsx_runtime_1.jsx)("hr", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Version:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "The currently active version of this package. Manage versions in the Versions tab." })] }), activeVersion ? ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mt-1 mb-3", children: [(0, jsx_runtime_1.jsxs)("strong", { className: "me-2", children: ["v", activeVersion.version] }), activeVersion.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${activeVersion.versionTag === "dev" ? "text-bg-danger" : activeVersion.versionTag.startsWith("beta") ? "text-bg-warning" : "text-bg-success"} me-2`, children: activeVersion.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: activeVersion.packageVersionHash?.substring(0, 8) }), newerLevel === "uptodate" ? ((0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-success", children: "Up to date" })) : newerLevel ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge text-bg-${newerLevel === "major" ? "danger" : newerLevel === "minor" ? "warning" : "info"}`, children: ["Newer ", newerLevel, " version available"] })) : null] })) : ((0, jsx_runtime_1.jsx)("div", { className: "text-muted small mt-1 mb-3", children: "No active version" })), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Auto-Update Range:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Controls which new versions are auto-activated for the group. When an admin device auto-upgrades, the group's active version advances for everyone. **Pinned** = never auto-update. **Patch** = same major.minor (e.g. 1.2.x). **Minor** = same major (e.g. 1.x.x). **Latest** = always auto-update to newest." })] }), (0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm", value: groupRange, onChange: async (e) => {
77
78
  const val = e.target.value;
78
79
  const current = await (0, peers_sdk_1.Packages)().get(pkg.packageId);
@@ -1,3 +1,5 @@
1
- /** Searchable list of installed packages, plus create/import from the search box. */
2
- /** Packages screen: search, local folder grants, and the installed package list. */
1
+ /**
2
+ * Packages screen: search, create/import/install from the search box, local folder grants,
3
+ * and the installed package list.
4
+ */
3
5
  export declare const PackageList: () => import("react/jsx-runtime").JSX.Element;