mirror of
https://github.com/logos-co/logos-package-manager-module.git
synced 2026-08-27 10:31:10 +00:00
implement inspectPackage, uninstallPackage, resolveDependencies, resolveDependents, and gated uninstall/upgrade (#47)
* implement inspectPackage, uninstallPackage, resolveDependencies, resolveDependents, and gated uninstall/upgrade * fix: require ack before confirmUpgrade proceeds Agent-Logs-Url: https://github.com/logos-co/logos-package-manager-module/sessions/2fc80a97-a06e-4a07-99f2-11d4a7a9ea99 Co-authored-by: dlipicar <11161531+dlipicar@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * pr comments * add tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dlipicar <11161531+dlipicar@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
dlipicar
Copilot
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent
34ef2a140a
commit
aead48cefd
@@ -28,11 +28,13 @@ All methods are accessible via LogosAPI from other modules and UI plugins.
|
||||
| `setUserModulesDirectory(dir)` | Set directory for user-installed core modules |
|
||||
| `setUserUiPluginsDirectory(dir)` | Set directory for user-installed UI plugins |
|
||||
|
||||
### Installation
|
||||
### Installation & Inspection
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `installPlugin(path, skipIfNotNewer)` | `QVariantMap` | Install a local `.lgx` file. Returns `{name, path, isCoreModule, signatureStatus, error}`. When `signatureStatus` is `"signed"` or `"invalid"`, also includes `signerDid`, `signerName`, `signerUrl`, `trustedAs`. When `signatureStatus` is `"error"`, includes `signatureError`. |
|
||||
| `inspectPackage(lgxPath)` | `QVariantMap` | Inspect an LGX file **without installing**. Returns metadata + install status: `{name, version, type, description, category, rootHash, signatureStatus, signerDid?, signerName?, isAlreadyInstalled, installedVersion?, installedHash?, installedDependents?, variants}`. `rootHash` is the Merkle tree root from `manifest.hashes.root` — the same identifier the online catalog exposes. When `isAlreadyInstalled` is true, `installedHash` is the corresponding value from the on-disk manifest. Used by callers (e.g. Basecamp) to show a confirmation dialog before committing. |
|
||||
| `uninstallPackage(packageName)` | `QVariantMap` | Remove a user-installed package immediately (ungated). Refuses embedded packages. Returns `{success, error?, removedFiles?}`. On success emits `corePluginUninstalled` or `uiPluginUninstalled`. **Headless callers** (lgpm, scripts) should use this. GUI callers should prefer `requestUninstall` below. |
|
||||
|
||||
### Scanning
|
||||
|
||||
@@ -43,7 +45,43 @@ All methods are accessible via LogosAPI from other modules and UI plugins.
|
||||
| `getInstalledUiPlugins()` | `QVariantList` | Installed UI plugins only |
|
||||
| `getValidVariants()` | `QStringList` | Platform variants this build accepts (e.g. `["darwin-arm64-dev"]`) |
|
||||
|
||||
Each item in the scan results contains all `manifest.json` fields plus `installDir` and `mainFilePath`.
|
||||
Each item in the scan results contains all `manifest.json` fields plus `installDir`, `mainFilePath`, and `installType` (`"embedded"` or `"user"`).
|
||||
|
||||
### Dependency Resolution
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `resolveDependencies(packageName, recursive)` | `QVariantMap` | Forward dependency tree rooted at `packageName`. Shape: `{name, status, version, installType, children: [...]}`. `recursive=false` walks only depth-1 (children have empty `children`); `recursive=true` walks the full tree, stopping at NotInstalled/Cycle nodes. Unknown root → `{}`. |
|
||||
| `resolveDependents(packageName, recursive)` | `QVariantMap` | Reverse dependency tree rooted at `packageName`. Shape: `{name, version, type, installType, installDir, children: [...]}`. Same depth semantics as `resolveDependencies`. Unknown root → `{}`. |
|
||||
| `resolveFlatDependencies(packageName, recursive)` | `QVariantList` | Flat projection of the forward walk. Each entry: `{name, status, version, installType}` (no `children`). `recursive=false` → direct children only; `recursive=true` → every descendant, BFS-ordered, deduped by name. |
|
||||
| `resolveFlatDependents(packageName, recursive)` | `QVariantList` | Flat projection of the reverse walk. Each entry: `{name, version, type, installType, installDir}`. Same `recursive` semantics as `resolveFlatDependencies`. |
|
||||
|
||||
### Gated Uninstall / Upgrade Flow
|
||||
|
||||
For **GUI callers** that need to show a confirmation dialog before destructive operations. The protocol ensures destructive work never runs without a live listener driving the dialog.
|
||||
|
||||
**Protocol:**
|
||||
|
||||
1. Caller invokes `requestUninstall(name)` or `requestUpgrade(name, releaseTag, mode)`. Returns `{success, error?}` synchronously. On success, sets pending state, emits `beforeUninstall` / `beforeUpgrade`, and starts a **3-second ack timer**.
|
||||
|
||||
2. A listener receiving the event **must immediately** call `ackPendingAction(name)` to cancel the ack timer. This says "I'm driving the dialog — wait indefinitely for the user decision."
|
||||
|
||||
3. If the ack timer fires without an ack (no listener present, or event loop stalled >3s), the module clears pending state and emits `uninstallCancelled` / `upgradeCancelled`. No files are removed.
|
||||
|
||||
4. Once acked, the listener shows a confirmation dialog. On user confirm: `confirmUninstall(name)` / `confirmUpgrade(name, releaseTag)`. On cancel: `cancelUninstall(name)` / `cancelUpgrade(name, releaseTag)`.
|
||||
|
||||
Only **one** gated flow can be pending globally (across all packages and both operations). A second `requestXxx` while one is pending returns `{success: false, error: "Another <op> is in progress for '<name>'"}`.
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `requestUninstall(name)` | `QVariantMap` | Start gated uninstall. Emits `beforeUninstall`, starts ack timer. |
|
||||
| `requestUpgrade(name, releaseTag, mode)` | `QVariantMap` | Start gated upgrade. `mode`: 0=upgrade, 1=downgrade, 2=sidegrade. Emits `beforeUpgrade`, starts ack timer. |
|
||||
| `ackPendingAction(name)` | `QVariantMap` | Acknowledge receipt of a `before*` event. Cancels the ack timer. Idempotent. |
|
||||
| `confirmUninstall(name)` | `QVariantMap` | Proceed with uninstall. Removes files, emits `corePluginUninstalled` / `uiPluginUninstalled`. |
|
||||
| `cancelUninstall(name)` | `QVariantMap` | Abort uninstall. Emits `uninstallCancelled(name, "user cancelled")`. |
|
||||
| `confirmUpgrade(name, releaseTag)` | `QVariantMap` | Proceed with upgrade. Uninstalls old version, emits `upgradeUninstallDone` for the caller to drive the download+install of the new version. |
|
||||
| `cancelUpgrade(name, releaseTag)` | `QVariantMap` | Abort upgrade. Emits `upgradeCancelled(name, releaseTag, "user cancelled")`. |
|
||||
| `resetPendingAction()` | `QVariantMap` | Clear any pending state. Called by Basecamp at startup to recover from a prior crash mid-dialog. |
|
||||
|
||||
### Signature Policy
|
||||
|
||||
@@ -63,10 +101,24 @@ Each item in the scan results contains all `manifest.json` fields plus `installD
|
||||
|
||||
### Events
|
||||
|
||||
**Installation events:**
|
||||
|
||||
| Event | Data | Description |
|
||||
|-------|------|-------------|
|
||||
| `corePluginFileInstalled` | `[path]` | Emitted after a core module `.lgx` is installed |
|
||||
| `uiPluginFileInstalled` | `[path]` | Emitted after a UI plugin `.lgx` is installed |
|
||||
| `corePluginUninstalled` | `[name]` | Emitted after a core module is uninstalled |
|
||||
| `uiPluginUninstalled` | `[name]` | Emitted after a UI plugin is uninstalled |
|
||||
|
||||
**Gated flow events** (see "Gated Uninstall / Upgrade Flow" above):
|
||||
|
||||
| Event | Data | Description |
|
||||
|-------|------|-------------|
|
||||
| `beforeUninstall` | `{name, installedDependents}` | A gated uninstall was requested. Listener must ack within 3s. |
|
||||
| `beforeUpgrade` | `{name, releaseTag, mode, installedDependents}` | A gated upgrade was requested. Listener must ack within 3s. |
|
||||
| `uninstallCancelled` | `{name, reason}` | Uninstall was cancelled — either by ack timeout or user cancel. |
|
||||
| `upgradeCancelled` | `{name, releaseTag, reason}` | Upgrade was cancelled — either by ack timeout or user cancel. |
|
||||
| `upgradeUninstallDone` | `{name, releaseTag, mode}` | Old version uninstalled during upgrade; caller should now download+install the new version. |
|
||||
|
||||
### Usage from another module
|
||||
|
||||
@@ -101,13 +153,37 @@ logos.package_manager.removeTrustedKey("logos-official");
|
||||
QVariantMap sigInfo = logos.package_manager.verifyPackage("/path/to/waku_module.lgx");
|
||||
// sigInfo: {isSigned, signatureValid, packageValid, signerDid, signerName, signerUrl, trustedAs, error}
|
||||
|
||||
// Install a downloaded .lgx file
|
||||
// Inspect an LGX before installing (shows metadata in a confirmation dialog)
|
||||
QVariantMap info = logos.package_manager.inspectPackage("/path/to/waku_module.lgx");
|
||||
// info: {name, version, type, signatureStatus, isAlreadyInstalled, installedVersion?, ...}
|
||||
|
||||
// Install a downloaded .lgx file (ungated — for headless/scripted use)
|
||||
QVariantMap result = logos.package_manager.installPlugin("/path/to/waku_module.lgx", false);
|
||||
if (result.contains("error")) {
|
||||
qWarning() << "Install failed:" << result["error"].toString();
|
||||
}
|
||||
// result also includes: signatureStatus ("signed"/"unsigned"/"invalid"), signerDid, signerName, signerUrl, trustedAs
|
||||
|
||||
// Flat deduped list of every reverse dependent (BFS over the reverse tree).
|
||||
QVariantList dependents = logos.package_manager.resolveFlatDependents("my_module", true);
|
||||
// Or fetch the tree shape directly when you want parent/child structure.
|
||||
QVariantMap dependentsTree = logos.package_manager.resolveDependents("my_module", true);
|
||||
|
||||
// GUI-mode gated uninstall (requires a listener to drive the confirmation dialog)
|
||||
logos.package_manager.requestUninstallAsync("my_module", [](QVariantMap r) {
|
||||
if (!r.value("success").toBool()) qWarning() << r.value("error").toString();
|
||||
});
|
||||
|
||||
// Listen for gated flow events
|
||||
logos.package_manager.on("beforeUninstall", [&logos](const QVariantList& data) {
|
||||
QString name = data[0].toMap().value("name").toString();
|
||||
// Ack immediately to cancel the 3s timer
|
||||
logos.package_manager.ackPendingActionAsync(name, [](QVariantMap) {});
|
||||
// Show dialog... then confirm or cancel:
|
||||
// logos.package_manager.confirmUninstallAsync(name, ...);
|
||||
// logos.package_manager.cancelUninstallAsync(name, ...);
|
||||
});
|
||||
|
||||
// Listen for installation events
|
||||
logos.package_manager.on("corePluginFileInstalled", [](const QVariantList& data) {
|
||||
QString path = data[0].toString();
|
||||
|
||||
Generated
+132
-73
@@ -94,11 +94,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775831954,
|
||||
"narHash": "sha256-XrYR9l3YZSfX/f90Fsedzz3XZufWfP75OaJQnHIH29Q=",
|
||||
"lastModified": 1776101366,
|
||||
"narHash": "sha256-HxkzOs2xv0grkNAJMBLXKDjVl8Z+z3YFn+sC4eFKy/8=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-cpp-sdk",
|
||||
"rev": "2a21637e0238b4e629f4b707d13f0b803810cc7d",
|
||||
"rev": "1468180b2567f4c59346bb94f74951e76341f5c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -201,11 +201,11 @@
|
||||
"process-stats": "process-stats"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775764743,
|
||||
"narHash": "sha256-nPWoaPLoeE+pjPg9fP5sc5038ZenbePlD6Hl0/Q4Rpc=",
|
||||
"lastModified": 1776084938,
|
||||
"narHash": "sha256-0UL6tG6mK00HN99fm9CLJu3JA9ay2ry6dgeHfyApiWo=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-liblogos",
|
||||
"rev": "522c56b4fafb4ef30615231ef6616316a104b935",
|
||||
"rev": "b293e9d70a04983778ef2ef3ef42596f76f41161",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -256,11 +256,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1776101574,
|
||||
"narHash": "sha256-VfvZW5efQtgt8QOJhS35btP6beG+E/UrnfP9YMRItEw=",
|
||||
"lastModified": 1776372591,
|
||||
"narHash": "sha256-uuY3FvSbWF11DR7JO9jyMDV25fLJoxT9TpDW7l/BngM=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-module-builder",
|
||||
"rev": "d3616e8410320a2bddc39e56348c64e7dfffb918",
|
||||
"rev": "e14aaa89f9ef652daf201cfaf3d06817ac8a440e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -709,11 +709,11 @@
|
||||
"nixpkgs": "nixpkgs_25"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -745,11 +745,11 @@
|
||||
"nixpkgs": "nixpkgs_27"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -781,11 +781,11 @@
|
||||
"nixpkgs": "nixpkgs_29"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -835,11 +835,11 @@
|
||||
"nixpkgs": "nixpkgs_31"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -871,11 +871,11 @@
|
||||
"nixpkgs": "nixpkgs_33"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -907,11 +907,11 @@
|
||||
"nixpkgs": "nixpkgs_35"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -979,11 +979,11 @@
|
||||
"nixpkgs": "nixpkgs_39"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1015,11 +1015,11 @@
|
||||
"nixpkgs": "nixpkgs_40"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1051,11 +1051,11 @@
|
||||
"nixpkgs": "nixpkgs_42"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1069,11 +1069,11 @@
|
||||
"nixpkgs": "nixpkgs_43"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"lastModified": 1774455309,
|
||||
"narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1136,6 +1136,24 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-nix_47": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_47"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1773955630,
|
||||
"narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-nix_5": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_5"
|
||||
@@ -1284,7 +1302,7 @@
|
||||
},
|
||||
"logos-package-manager_2": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_34",
|
||||
"logos-nix": "logos-nix_35",
|
||||
"logos-package": "logos-package_4",
|
||||
"nix-bundle-appimage": "nix-bundle-appimage_2",
|
||||
"nix-bundle-dir": "nix-bundle-dir_6",
|
||||
@@ -1312,7 +1330,7 @@
|
||||
},
|
||||
"logos-package-manager_3": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_42",
|
||||
"logos-nix": "logos-nix_43",
|
||||
"logos-package": "logos-package_6",
|
||||
"nix-bundle-appimage": "nix-bundle-appimage_3",
|
||||
"nix-bundle-dir": "nix-bundle-dir_9",
|
||||
@@ -1323,11 +1341,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775680583,
|
||||
"narHash": "sha256-0Bh48zTfi4lPL78ZLgmiX+QMW+nvjWKXHp5iJPEhvLg=",
|
||||
"lastModified": 1776374462,
|
||||
"narHash": "sha256-HMkuqSLdScAWTwXEWjhqx9Yk82GiPzPIfRaHTvjG730=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-package-manager",
|
||||
"rev": "8110734252edf9ca4266f475ace1c7c9bee68018",
|
||||
"rev": "9101875bc103214855bc6217834e22e66802ed86",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1338,7 +1356,7 @@
|
||||
},
|
||||
"logos-package_2": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_27",
|
||||
"logos-nix": "logos-nix_28",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"logos-standalone-app",
|
||||
@@ -1364,7 +1382,7 @@
|
||||
},
|
||||
"logos-package_3": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_31",
|
||||
"logos-nix": "logos-nix_32",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-lgx",
|
||||
@@ -1389,7 +1407,7 @@
|
||||
},
|
||||
"logos-package_4": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_35",
|
||||
"logos-nix": "logos-nix_36",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-logos-module-install",
|
||||
@@ -1415,7 +1433,7 @@
|
||||
},
|
||||
"logos-package_5": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_40",
|
||||
"logos-nix": "logos-nix_41",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-logos-module-install",
|
||||
@@ -1441,7 +1459,7 @@
|
||||
},
|
||||
"logos-package_6": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_43",
|
||||
"logos-nix": "logos-nix_44",
|
||||
"nixpkgs": [
|
||||
"logos-package-manager",
|
||||
"logos-package",
|
||||
@@ -1450,11 +1468,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775677349,
|
||||
"narHash": "sha256-G+0E1mkmG3QDeTR4Pgy+xkiole/TDq+FYrvHwNp9Yrc=",
|
||||
"lastModified": 1775835037,
|
||||
"narHash": "sha256-Cti0DhkzyLQs98BSzcHWMLtGXpa3n+R+5upfSw6vKdQ=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-package",
|
||||
"rev": "64edea0e64309e1c9f91259d16f8f81e5e39e40e",
|
||||
"rev": "ff93a0df15ceab255f27687d22d962ea2737efbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1513,6 +1531,30 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-qt-mcp": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_25",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"logos-standalone-app",
|
||||
"logos-nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774455349,
|
||||
"narHash": "sha256-rebrtH1UxC1hDuwQBwyYbGzNCrnuuqiVL7OvzUhk65k=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-qt-mcp",
|
||||
"rev": "c5223b4b640add09e461983b8fddbd12c8b31f4f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-qt-mcp",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-standalone-app": {
|
||||
"inputs": {
|
||||
"logos-capability-module": "logos-capability-module",
|
||||
@@ -1520,6 +1562,7 @@
|
||||
"logos-design-system": "logos-design-system",
|
||||
"logos-liblogos": "logos-liblogos",
|
||||
"logos-nix": "logos-nix_24",
|
||||
"logos-qt-mcp": "logos-qt-mcp",
|
||||
"logos-view-module-runtime": "logos-view-module-runtime",
|
||||
"nix-bundle-lgx": "nix-bundle-lgx",
|
||||
"nixpkgs": [
|
||||
@@ -1530,11 +1573,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775836431,
|
||||
"narHash": "sha256-2zQcNzbO3RnC8zZBB2y1s1ItZNwYzqgJjlevYjzKQHQ=",
|
||||
"lastModified": 1776372380,
|
||||
"narHash": "sha256-kleKGfcRgtIjeltogH0NCFrp5QwetlXsHJxOTllP13E=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-standalone-app",
|
||||
"rev": "84d24c6a0d5c73bb81160e2a13de9995caa34c0f",
|
||||
"rev": "5e122402898c1d896cc6b72db04c6277697a2f2f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1549,7 +1592,7 @@
|
||||
"logos-module-builder",
|
||||
"logos-cpp-sdk"
|
||||
],
|
||||
"logos-nix": "logos-nix_29",
|
||||
"logos-nix": "logos-nix_30",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"logos-test-framework",
|
||||
@@ -1578,7 +1621,7 @@
|
||||
"logos-standalone-app",
|
||||
"logos-cpp-sdk"
|
||||
],
|
||||
"logos-nix": "logos-nix_25",
|
||||
"logos-nix": "logos-nix_26",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"logos-standalone-app",
|
||||
@@ -1587,11 +1630,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775835902,
|
||||
"narHash": "sha256-jQ1ZJis3stGgj1jQvQI0JuxPpzeZtilAwLEUVqT0lkA=",
|
||||
"lastModified": 1776372308,
|
||||
"narHash": "sha256-21SqqdOuHBLUGcYxGvjtC4iKp+wLGEQOKn64qLVl/+0=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-view-module-runtime",
|
||||
"rev": "b5799b743ef92afefacb8510a2935fc9585c308d",
|
||||
"rev": "5dc32e0131e9abf0a86c085119aa082d56486d9e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1630,7 +1673,7 @@
|
||||
},
|
||||
"nix-bundle-appimage_2": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_36",
|
||||
"logos-nix": "logos-nix_37",
|
||||
"nix-bundle-dir": "nix-bundle-dir_5",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
@@ -1657,7 +1700,7 @@
|
||||
},
|
||||
"nix-bundle-appimage_3": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_44",
|
||||
"logos-nix": "logos-nix_45",
|
||||
"nix-bundle-dir": "nix-bundle-dir_8",
|
||||
"nixpkgs": [
|
||||
"logos-package-manager",
|
||||
@@ -1735,7 +1778,7 @@
|
||||
},
|
||||
"nix-bundle-dir_3": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_28",
|
||||
"logos-nix": "logos-nix_29",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"logos-standalone-app",
|
||||
@@ -1761,7 +1804,7 @@
|
||||
},
|
||||
"nix-bundle-dir_4": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_32",
|
||||
"logos-nix": "logos-nix_33",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-lgx",
|
||||
@@ -1786,7 +1829,7 @@
|
||||
},
|
||||
"nix-bundle-dir_5": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_37",
|
||||
"logos-nix": "logos-nix_38",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-logos-module-install",
|
||||
@@ -1811,7 +1854,7 @@
|
||||
},
|
||||
"nix-bundle-dir_6": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_38",
|
||||
"logos-nix": "logos-nix_39",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-logos-module-install",
|
||||
@@ -1837,7 +1880,7 @@
|
||||
},
|
||||
"nix-bundle-dir_7": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_41",
|
||||
"logos-nix": "logos-nix_42",
|
||||
"nixpkgs": [
|
||||
"logos-module-builder",
|
||||
"nix-bundle-logos-module-install",
|
||||
@@ -1863,7 +1906,7 @@
|
||||
},
|
||||
"nix-bundle-dir_8": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_45",
|
||||
"logos-nix": "logos-nix_46",
|
||||
"nixpkgs": [
|
||||
"logos-package-manager",
|
||||
"nix-bundle-appimage",
|
||||
@@ -1886,7 +1929,7 @@
|
||||
},
|
||||
"nix-bundle-dir_9": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_46",
|
||||
"logos-nix": "logos-nix_47",
|
||||
"nixpkgs": [
|
||||
"logos-package-manager",
|
||||
"nix-bundle-dir",
|
||||
@@ -1910,7 +1953,7 @@
|
||||
},
|
||||
"nix-bundle-lgx": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_26",
|
||||
"logos-nix": "logos-nix_27",
|
||||
"logos-package": "logos-package_2",
|
||||
"nix-bundle-dir": "nix-bundle-dir_3",
|
||||
"nixpkgs": [
|
||||
@@ -1936,7 +1979,7 @@
|
||||
},
|
||||
"nix-bundle-lgx_2": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_30",
|
||||
"logos-nix": "logos-nix_31",
|
||||
"logos-package": "logos-package_3",
|
||||
"nix-bundle-dir": "nix-bundle-dir_4",
|
||||
"nixpkgs": [
|
||||
@@ -1962,7 +2005,7 @@
|
||||
},
|
||||
"nix-bundle-lgx_3": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_39",
|
||||
"logos-nix": "logos-nix_40",
|
||||
"logos-package": "logos-package_5",
|
||||
"nix-bundle-dir": "nix-bundle-dir_7",
|
||||
"nixpkgs": [
|
||||
@@ -1989,7 +2032,7 @@
|
||||
},
|
||||
"nix-bundle-logos-module-install": {
|
||||
"inputs": {
|
||||
"logos-nix": "logos-nix_33",
|
||||
"logos-nix": "logos-nix_34",
|
||||
"logos-package-manager": "logos-package-manager_2",
|
||||
"nix-bundle-lgx": "nix-bundle-lgx_3",
|
||||
"nixpkgs": [
|
||||
@@ -2669,6 +2712,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_47": {
|
||||
"locked": {
|
||||
"lastModified": 1759036355,
|
||||
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1759036355,
|
||||
|
||||
@@ -2,8 +2,121 @@
|
||||
#include <package_manager_lib.h>
|
||||
#include <lgx.h>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Struct → LogosMap / LogosList conversion helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Wire format is identical to what PackageManagerLib / the lgpm CLI emit via
|
||||
// package_manager_json.cpp's nlohmann ADL hooks. We re-hand-roll here (rather
|
||||
// than reuse those hooks) so the unit tests can compile against the stub
|
||||
// header in tests/stubs/package_manager_lib.h without pulling in the lib's
|
||||
// JSON module.
|
||||
//
|
||||
// Keep these in sync with package_manager_json.cpp's to_json definitions.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
LogosMap toLogosMap(const Hashes& h)
|
||||
{
|
||||
LogosMap m = LogosMap::object();
|
||||
m["root"] = h.root;
|
||||
return m;
|
||||
}
|
||||
|
||||
LogosMap toLogosMap(const InstalledPackage& p)
|
||||
{
|
||||
LogosMap m = LogosMap::object();
|
||||
m["name"] = p.name;
|
||||
m["version"] = p.version;
|
||||
m["description"] = p.description;
|
||||
m["type"] = p.type;
|
||||
m["category"] = p.category;
|
||||
m["author"] = p.author;
|
||||
m["license"] = p.license;
|
||||
m["icon"] = p.icon;
|
||||
m["view"] = p.view;
|
||||
|
||||
LogosList deps = LogosList::array();
|
||||
for (const auto& d : p.dependencies) deps.push_back(d);
|
||||
m["dependencies"] = deps;
|
||||
|
||||
m["hashes"] = toLogosMap(p.hashes);
|
||||
m["installType"] = std::string(installTypeToString(p.installType));
|
||||
m["installDir"] = p.installDir;
|
||||
m["mainFilePath"] = p.mainFilePath;
|
||||
return m;
|
||||
}
|
||||
|
||||
LogosList toLogosList(const std::vector<InstalledPackage>& v)
|
||||
{
|
||||
LogosList out = LogosList::array();
|
||||
for (const auto& p : v) out.push_back(toLogosMap(p));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Flat per-node projection — just the node's own fields, no `children`.
|
||||
// Shared between the flat list APIs (resolveFlatDependencies /
|
||||
// resolveFlatDependents) and the tree APIs (where each recursive step is
|
||||
// "this node's fields plus its children").
|
||||
LogosMap toFlatLogosMap(const DependencyTreeNode& n)
|
||||
{
|
||||
LogosMap m = LogosMap::object();
|
||||
m["name"] = n.name;
|
||||
m["status"] = std::string(dependencyStatusToString(n.status));
|
||||
if (n.status == DependencyStatus::Installed) {
|
||||
m["version"] = n.version;
|
||||
m["installType"] = std::string(installTypeToString(n.installType));
|
||||
} else {
|
||||
m["version"] = "";
|
||||
m["installType"] = "";
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
LogosMap toFlatLogosMap(const DependentTreeNode& n)
|
||||
{
|
||||
LogosMap m = LogosMap::object();
|
||||
m["name"] = n.name;
|
||||
m["version"] = n.version;
|
||||
m["type"] = n.type;
|
||||
m["installType"] = std::string(installTypeToString(n.installType));
|
||||
m["installDir"] = n.installDir;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Depth-clipped tree serialisation — root always emitted with its own
|
||||
// fields; `maxDepth` bounds how far we recurse into `children`. Used by
|
||||
// resolveDependencies/resolveDependents: maxDepth=1 for !recursive (root
|
||||
// + direct children with empty children arrays), maxDepth=INT_MAX for
|
||||
// the full tree.
|
||||
template <typename Node>
|
||||
LogosMap toLogosTreeMap(const Node& n, int maxDepth)
|
||||
{
|
||||
LogosMap m = toFlatLogosMap(n);
|
||||
LogosList children = LogosList::array();
|
||||
if (maxDepth > 0) {
|
||||
for (const auto& c : n.children)
|
||||
children.push_back(toLogosTreeMap(c, maxDepth - 1));
|
||||
}
|
||||
m["children"] = children;
|
||||
return m;
|
||||
}
|
||||
|
||||
template <typename Node>
|
||||
LogosList toFlatLogosList(const std::vector<Node>& v)
|
||||
{
|
||||
LogosList out = LogosList::array();
|
||||
for (const auto& n : v) out.push_back(toFlatLogosMap(n));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PackageManagerImpl::PackageManagerImpl()
|
||||
: m_lib(nullptr)
|
||||
@@ -13,6 +126,19 @@ PackageManagerImpl::PackageManagerImpl()
|
||||
|
||||
PackageManagerImpl::~PackageManagerImpl()
|
||||
{
|
||||
// Signal any running worker thread to exit, then join it before
|
||||
// tearing down state it might still reference (m_pendingAction,
|
||||
// emitEvent). The lock is taken briefly to publish m_ackShutdown and
|
||||
// bump m_ackGeneration atomically; notify + join happen outside the
|
||||
// lock so the worker can re-acquire and exit its wait_for.
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_stateMutex);
|
||||
m_ackShutdown = true;
|
||||
++m_ackGeneration;
|
||||
}
|
||||
m_ackCv.notify_all();
|
||||
if (m_ackThread.joinable()) m_ackThread.join();
|
||||
|
||||
delete m_lib;
|
||||
m_lib = nullptr;
|
||||
}
|
||||
@@ -71,19 +197,199 @@ LogosMap PackageManagerImpl::installPlugin(const std::string& pluginPath, bool s
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::inspectPackage(const std::string& lgxPath)
|
||||
{
|
||||
LogosMap result;
|
||||
|
||||
lgx_package_t pkg = lgx_load(lgxPath.c_str());
|
||||
if (!pkg) {
|
||||
result["error"] = std::string("Failed to load LGX package: ")
|
||||
+ (lgx_get_last_error() ? lgx_get_last_error() : "unknown");
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* rawName = lgx_get_name(pkg);
|
||||
const char* rawVersion = lgx_get_version(pkg);
|
||||
const char* rawDesc = lgx_get_description(pkg);
|
||||
const char* rawManifest = lgx_get_manifest_json(pkg);
|
||||
|
||||
std::string pkgName = rawName ? rawName : "";
|
||||
std::string pkgVersion = rawVersion ? rawVersion : "";
|
||||
|
||||
result["name"] = pkgName;
|
||||
result["version"] = pkgVersion;
|
||||
result["description"] = rawDesc ? std::string(rawDesc) : "";
|
||||
|
||||
// Extract type, category, and root content hash from the embedded
|
||||
// manifest. The root hash (Merkle tree root over the package content,
|
||||
// `manifest.hashes.root`) is the same identifier PMU renders when
|
||||
// browsing the online catalog — surfacing it here lets the install
|
||||
// confirmation dialog show a stable per-release fingerprint.
|
||||
if (rawManifest) {
|
||||
try {
|
||||
auto doc = LogosMap::parse(rawManifest);
|
||||
result["type"] = doc.value("type", "");
|
||||
result["category"] = doc.value("category", "");
|
||||
if (doc.contains("hashes") && doc["hashes"].is_object()) {
|
||||
result["rootHash"] = doc["hashes"].value("root", "");
|
||||
} else {
|
||||
result["rootHash"] = "";
|
||||
}
|
||||
} catch (...) {
|
||||
result["type"] = "";
|
||||
result["category"] = "";
|
||||
result["rootHash"] = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Available platform variants.
|
||||
const char** variants = lgx_get_variants(pkg);
|
||||
LogosList variantList = LogosList::array();
|
||||
if (variants) {
|
||||
for (int i = 0; variants[i]; ++i)
|
||||
variantList.push_back(std::string(variants[i]));
|
||||
lgx_free_string_array(variants);
|
||||
}
|
||||
result["variants"] = variantList;
|
||||
|
||||
lgx_free_package(pkg);
|
||||
|
||||
// Signature verification — standalone, no install side effects.
|
||||
auto sig = m_lib->verifyPackageSignature(lgxPath);
|
||||
if (sig.is_signed) {
|
||||
bool valid = sig.signature_valid && sig.package_valid;
|
||||
result["signatureStatus"] = valid ? std::string("signed")
|
||||
: std::string("invalid");
|
||||
result["signerDid"] = sig.signer_did;
|
||||
result["signerName"] = sig.signer_name;
|
||||
} else if (!sig.error.empty()) {
|
||||
result["signatureStatus"] = std::string("error");
|
||||
} else {
|
||||
result["signatureStatus"] = std::string("unsigned");
|
||||
}
|
||||
|
||||
// Check if this package is already installed.
|
||||
bool isAlreadyInstalled = false;
|
||||
std::string installedVersion;
|
||||
std::string installedHash;
|
||||
std::vector<InstalledPackage> scan = m_lib->getInstalledPackages();
|
||||
for (const auto& entry : scan) {
|
||||
if (entry.name == pkgName) {
|
||||
isAlreadyInstalled = true;
|
||||
installedVersion = entry.version;
|
||||
// Passthrough from the installed manifest.json; same field PMU
|
||||
// reads in the online catalog (`manifest.hashes.root`).
|
||||
installedHash = entry.hashes.root;
|
||||
break;
|
||||
}
|
||||
}
|
||||
result["isAlreadyInstalled"] = isAlreadyInstalled;
|
||||
result["installedVersion"] = installedVersion;
|
||||
result["installedHash"] = installedHash;
|
||||
|
||||
// If already installed, compute reverse dependents so the dialog can
|
||||
// show what would be affected by an upgrade.
|
||||
if (isAlreadyInstalled) {
|
||||
auto deps = installedDependentsNames(pkgName);
|
||||
LogosList depList = LogosList::array();
|
||||
for (const auto& d : deps) depList.push_back(d);
|
||||
result["installedDependents"] = depList;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
LogosList PackageManagerImpl::getInstalledPackages()
|
||||
{
|
||||
return LogosList::parse(m_lib->getInstalledPackages());
|
||||
return toLogosList(m_lib->getInstalledPackages());
|
||||
}
|
||||
|
||||
LogosList PackageManagerImpl::getInstalledModules()
|
||||
{
|
||||
return LogosList::parse(m_lib->getInstalledModules());
|
||||
return toLogosList(m_lib->getInstalledModules());
|
||||
}
|
||||
|
||||
LogosList PackageManagerImpl::getInstalledUiPlugins()
|
||||
{
|
||||
return LogosList::parse(m_lib->getInstalledUiPlugins());
|
||||
return toLogosList(m_lib->getInstalledUiPlugins());
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::uninstallPackage(const std::string& packageName)
|
||||
{
|
||||
return doUninstall(packageName);
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::doUninstall(const std::string& packageName)
|
||||
{
|
||||
// Inspect the package before removal so we know whether to emit a core or UI event.
|
||||
std::vector<InstalledPackage> scan = m_lib->getInstalledPackages();
|
||||
std::string moduleType;
|
||||
for (const auto& entry : scan) {
|
||||
if (entry.name == packageName) {
|
||||
moduleType = entry.type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UninstallResult r = m_lib->uninstallPackage(packageName);
|
||||
|
||||
LogosMap response;
|
||||
response["success"] = r.success;
|
||||
if (!r.success) {
|
||||
response["error"] = r.errorMsg;
|
||||
} else {
|
||||
LogosList removed = LogosList::array();
|
||||
for (const auto& f : r.removedFiles) removed.push_back(f);
|
||||
response["removedFiles"] = removed;
|
||||
|
||||
if (emitEvent) {
|
||||
const std::string eventName =
|
||||
(moduleType == "core")
|
||||
? "corePluginUninstalled"
|
||||
: "uiPluginUninstalled";
|
||||
emitEvent(eventName, packageName);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::resolveDependencies(const std::string& packageName, bool recursive)
|
||||
{
|
||||
// Unknown roots surface as nullopt from the library; keep an empty
|
||||
// object on the wire so callers can `.contains(...)` without branching.
|
||||
auto tree = m_lib->resolveDependencies(packageName);
|
||||
if (!tree) return LogosMap::object();
|
||||
// maxDepth=1 clips to root + direct children (children with empty
|
||||
// `children` arrays); INT_MAX walks the full tree.
|
||||
return toLogosTreeMap(*tree, recursive ? std::numeric_limits<int>::max() : 1);
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::resolveDependents(const std::string& packageName, bool recursive)
|
||||
{
|
||||
// Same shape treatment as resolveDependencies — the library returns a
|
||||
// tree, we either clip it at depth 1 or walk the full reverse subtree.
|
||||
auto tree = m_lib->resolveDependents(packageName);
|
||||
if (!tree) return LogosMap::object();
|
||||
return toLogosTreeMap(*tree, recursive ? std::numeric_limits<int>::max() : 1);
|
||||
}
|
||||
|
||||
LogosList PackageManagerImpl::resolveFlatDependencies(const std::string& packageName, bool recursive)
|
||||
{
|
||||
// Flat list of per-node maps (no `children`). recursive=false emits
|
||||
// only the root's direct children; recursive=true emits every
|
||||
// descendant, BFS-ordered and deduped by name (via DependencyTreeNode::flatten()).
|
||||
auto tree = m_lib->resolveDependencies(packageName);
|
||||
if (!tree) return LogosList::array();
|
||||
return recursive ? toFlatLogosList(tree->flatten())
|
||||
: toFlatLogosList(tree->children);
|
||||
}
|
||||
|
||||
LogosList PackageManagerImpl::resolveFlatDependents(const std::string& packageName, bool recursive)
|
||||
{
|
||||
auto tree = m_lib->resolveDependents(packageName);
|
||||
if (!tree) return LogosList::array();
|
||||
return recursive ? toFlatLogosList(tree->flatten())
|
||||
: toFlatLogosList(tree->children);
|
||||
}
|
||||
|
||||
std::vector<std::string> PackageManagerImpl::getValidVariants()
|
||||
@@ -215,3 +521,409 @@ LogosList PackageManagerImpl::listTrustedKeys()
|
||||
lgx_free_keyring_list(list);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gated uninstall / upgrade flow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const char* PackageManagerImpl::opName(PendingOp op)
|
||||
{
|
||||
switch (op) {
|
||||
case PendingOp::Uninstall: return "uninstall";
|
||||
case PendingOp::Upgrade: return "upgrade";
|
||||
case PendingOp::None: return "none";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
bool PackageManagerImpl::isEmbedded(const std::string& packageName) const
|
||||
{
|
||||
std::vector<InstalledPackage> scan = m_lib->getInstalledPackages();
|
||||
for (const auto& entry : scan) {
|
||||
if (entry.name == packageName)
|
||||
return entry.installType == InstallType::Embedded;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> PackageManagerImpl::installedDependentsNames(const std::string& packageName) const
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
auto tree = m_lib->resolveDependents(packageName);
|
||||
if (!tree) return names;
|
||||
auto flat = tree->flatten();
|
||||
names.reserve(flat.size());
|
||||
for (const auto& d : flat) {
|
||||
if (!d.name.empty()) names.push_back(d.name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure-C++ ack timer — std::thread + std::condition_variable replacing QTimer.
|
||||
// See detailed protocol comment in the header.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void PackageManagerImpl::startAckTimerLocked(std::unique_lock<std::mutex>& lock)
|
||||
{
|
||||
// Precondition: caller holds m_stateMutex via `lock`.
|
||||
|
||||
// Bump the generation and wake any previously-running worker. If one is
|
||||
// still waiting on the CV, it'll re-acquire the mutex, see its captured
|
||||
// generation is stale, and bail.
|
||||
++m_ackGeneration;
|
||||
m_ackCv.notify_all();
|
||||
|
||||
// Join the previous worker (if any) before replacing m_ackThread —
|
||||
// assigning to a joinable std::thread is undefined behaviour. Release
|
||||
// the lock during join so the worker can proceed past its wait_for;
|
||||
// otherwise we'd deadlock (we hold the lock the worker needs).
|
||||
if (m_ackThread.joinable()) {
|
||||
lock.unlock();
|
||||
m_ackThread.join();
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
const uint64_t gen = m_ackGeneration;
|
||||
m_ackThread = std::thread([this, gen]() { ackTimerWorker(gen); });
|
||||
}
|
||||
|
||||
void PackageManagerImpl::stopAckTimerLocked()
|
||||
{
|
||||
// Caller holds m_stateMutex. Bump the generation and notify so any
|
||||
// running worker wakes up and exits silently. Do NOT join here: the
|
||||
// slot calling us is likely running on the module thread and the
|
||||
// worker might be mid-wait needing the mutex we hold. The worker
|
||||
// will exit on its own; the next startAckTimerLocked (or the
|
||||
// destructor) reaps the std::thread handle.
|
||||
++m_ackGeneration;
|
||||
m_ackCv.notify_all();
|
||||
}
|
||||
|
||||
void PackageManagerImpl::ackTimerWorker(uint64_t myGeneration)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_stateMutex);
|
||||
|
||||
// wait_for returns true when the predicate is satisfied, false on
|
||||
// timeout. Predicate: "stop waiting" — either the process is shutting
|
||||
// down or our generation is stale (a newer request / ack / cancel
|
||||
// has superseded us).
|
||||
bool cancelled = m_ackCv.wait_for(
|
||||
lock,
|
||||
std::chrono::milliseconds(m_ackTimeoutMs),
|
||||
[this, myGeneration]() {
|
||||
return m_ackShutdown || m_ackGeneration != myGeneration;
|
||||
}
|
||||
);
|
||||
if (cancelled) return;
|
||||
|
||||
// Full timeout with no cancellation — but recheck state now that we
|
||||
// hold the lock. An ack or a different state change could have
|
||||
// landed between the last CV check and here (unlikely, but cheap to
|
||||
// verify).
|
||||
if (m_ackShutdown) return;
|
||||
if (m_ackGeneration != myGeneration) return;
|
||||
if (m_pendingAction.op == PendingOp::None || m_pendingAction.acked) return;
|
||||
|
||||
// Claim the pending action so slot-side code sees a clean slate.
|
||||
PendingAction pa = m_pendingAction;
|
||||
m_pendingAction = {};
|
||||
|
||||
const std::string reason = "no listener acknowledged within "
|
||||
+ std::to_string(m_ackTimeoutMs) + "ms";
|
||||
|
||||
// Release the lock before emitting — emitEvent marshals through a
|
||||
// Qt signal; a listener synchronously calling back into this impl
|
||||
// (e.g. a headless runtime that calls uninstallPackage on cancel
|
||||
// notification) would otherwise re-enter the mutex and deadlock.
|
||||
lock.unlock();
|
||||
emitCancellation(pa, reason);
|
||||
}
|
||||
|
||||
void PackageManagerImpl::emitCancellation(const PendingAction& pa, const std::string& reason)
|
||||
{
|
||||
if (!emitEvent) return;
|
||||
|
||||
LogosMap payload;
|
||||
payload["name"] = pa.name;
|
||||
payload["reason"] = reason;
|
||||
if (pa.op == PendingOp::Upgrade) {
|
||||
payload["releaseTag"] = pa.releaseTag;
|
||||
emitEvent("upgradeCancelled", payload.dump());
|
||||
} else if (pa.op == PendingOp::Uninstall) {
|
||||
emitEvent("uninstallCancelled", payload.dump());
|
||||
}
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::requestUninstall(const std::string& packageName)
|
||||
{
|
||||
LogosMap response;
|
||||
// Empty packageName would be persisted into m_pendingAction.name and then
|
||||
// broadcast via beforeUninstall(payload{name:""}), causing listeners to
|
||||
// open a dialog titled "Uninstall ''?" with no dependents. Reject early
|
||||
// with a distinct error so callers can surface a sane toast and callers
|
||||
// that ARE the GUI can avoid showing a stray dialog.
|
||||
if (packageName.empty()) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Package name cannot be empty";
|
||||
return response;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(m_stateMutex);
|
||||
|
||||
if (m_pendingAction.op != PendingOp::None) {
|
||||
response["success"] = false;
|
||||
response["error"] = std::string("Another ") + opName(m_pendingAction.op)
|
||||
+ " is in progress for '" + m_pendingAction.name + "'";
|
||||
return response;
|
||||
}
|
||||
|
||||
if (isEmbedded(packageName)) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Cannot uninstall embedded module '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
|
||||
m_pendingAction = {};
|
||||
m_pendingAction.op = PendingOp::Uninstall;
|
||||
m_pendingAction.name = packageName;
|
||||
m_pendingAction.acked = false;
|
||||
|
||||
// Build the event payload while we still hold the lock (so m_lib reads
|
||||
// don't race against a concurrent slot). emitEvent itself is deferred
|
||||
// until after the unlock — see the reentrancy note in ackTimerWorker.
|
||||
LogosMap payload;
|
||||
payload["name"] = packageName;
|
||||
LogosList deps = LogosList::array();
|
||||
for (const auto& d : installedDependentsNames(packageName))
|
||||
deps.push_back(d);
|
||||
payload["installedDependents"] = deps;
|
||||
|
||||
// Start the ack timer (this may briefly release + re-acquire `lock`
|
||||
// while joining a previous worker).
|
||||
startAckTimerLocked(lock);
|
||||
|
||||
lock.unlock();
|
||||
if (emitEvent) emitEvent("beforeUninstall", payload.dump());
|
||||
|
||||
response["success"] = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::requestUpgrade(const std::string& packageName,
|
||||
const std::string& releaseTag,
|
||||
int64_t mode)
|
||||
{
|
||||
LogosMap response;
|
||||
// Same rationale as requestUninstall: empty name has to be rejected
|
||||
// before we set pending state, otherwise beforeUpgrade(name="") leads
|
||||
// listeners into an empty-title dialog.
|
||||
if (packageName.empty()) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Package name cannot be empty";
|
||||
return response;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(m_stateMutex);
|
||||
|
||||
if (m_pendingAction.op != PendingOp::None) {
|
||||
response["success"] = false;
|
||||
response["error"] = std::string("Another ") + opName(m_pendingAction.op)
|
||||
+ " is in progress for '" + m_pendingAction.name + "'";
|
||||
return response;
|
||||
}
|
||||
|
||||
if (isEmbedded(packageName)) {
|
||||
response["success"] = false;
|
||||
response["error"] = "Cannot upgrade embedded module '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
|
||||
m_pendingAction = {};
|
||||
m_pendingAction.op = PendingOp::Upgrade;
|
||||
m_pendingAction.name = packageName;
|
||||
m_pendingAction.releaseTag = releaseTag;
|
||||
m_pendingAction.mode = mode;
|
||||
m_pendingAction.acked = false;
|
||||
|
||||
LogosMap payload;
|
||||
payload["name"] = packageName;
|
||||
payload["releaseTag"] = releaseTag;
|
||||
payload["mode"] = mode;
|
||||
LogosList deps = LogosList::array();
|
||||
for (const auto& d : installedDependentsNames(packageName))
|
||||
deps.push_back(d);
|
||||
payload["installedDependents"] = deps;
|
||||
|
||||
startAckTimerLocked(lock);
|
||||
|
||||
lock.unlock();
|
||||
if (emitEvent) emitEvent("beforeUpgrade", payload.dump());
|
||||
|
||||
response["success"] = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::ackPendingAction(const std::string& packageName)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||
LogosMap response;
|
||||
if (m_pendingAction.op == PendingOp::None || m_pendingAction.name != packageName) {
|
||||
response["success"] = false;
|
||||
response["error"] = "No matching pending action to ack for '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
// Idempotent — re-acking an already-acked request is a no-op.
|
||||
m_pendingAction.acked = true;
|
||||
stopAckTimerLocked();
|
||||
response["success"] = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::confirmUninstall(const std::string& packageName)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||
if (m_pendingAction.op != PendingOp::Uninstall || m_pendingAction.name != packageName) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "No matching pending uninstall for '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
if (!m_pendingAction.acked) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "Pending uninstall for '" + packageName + "' has not been acknowledged";
|
||||
return response;
|
||||
}
|
||||
m_pendingAction = {};
|
||||
stopAckTimerLocked();
|
||||
}
|
||||
// Lock released before doUninstall — it emits corePluginUninstalled /
|
||||
// uiPluginUninstalled, and listeners may synchronously call back into
|
||||
// this impl (the whole point of the event is to trigger cleanup).
|
||||
return doUninstall(packageName);
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::cancelUninstall(const std::string& packageName)
|
||||
{
|
||||
PendingAction pa;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||
if (m_pendingAction.op != PendingOp::Uninstall || m_pendingAction.name != packageName) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "No matching pending uninstall for '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
// Symmetric with confirmUninstall: the gated protocol requires the
|
||||
// owning listener to ack before driving the decision either way.
|
||||
// An un-acked pending state is owned by the ack-reception timer;
|
||||
// letting cancel short-circuit it would bypass the protocol and
|
||||
// suppress the "no listener acknowledged" timeout event that
|
||||
// initiators otherwise rely on.
|
||||
if (!m_pendingAction.acked) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "Pending uninstall for '" + packageName + "' has not been acknowledged";
|
||||
return response;
|
||||
}
|
||||
pa = m_pendingAction;
|
||||
m_pendingAction = {};
|
||||
stopAckTimerLocked();
|
||||
}
|
||||
// Uniform cancellation notification — same event the ack-timeout path emits.
|
||||
// Initiators (PMU) subscribe once and handle every cancellation consistently.
|
||||
emitCancellation(pa, "user cancelled");
|
||||
LogosMap response;
|
||||
response["success"] = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::confirmUpgrade(const std::string& packageName,
|
||||
const std::string& releaseTag)
|
||||
{
|
||||
int64_t mode = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||
if (m_pendingAction.op != PendingOp::Upgrade
|
||||
|| m_pendingAction.name != packageName
|
||||
|| m_pendingAction.releaseTag != releaseTag) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "No matching pending upgrade for '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
if (!m_pendingAction.acked) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "Pending upgrade for '" + packageName + "' has not been acknowledged";
|
||||
return response;
|
||||
}
|
||||
mode = m_pendingAction.mode;
|
||||
m_pendingAction = {};
|
||||
stopAckTimerLocked();
|
||||
}
|
||||
|
||||
LogosMap uninstallResult = doUninstall(packageName);
|
||||
|
||||
// On successful uninstall, tell PMU to drive the download+install step
|
||||
// for the new version. The impl layer has no LogosAPI access (it only
|
||||
// communicates outward via the emitEvent callback), so we can't call
|
||||
// package_downloader directly. Instead we emit upgradeUninstallDone
|
||||
// with the pinned releaseTag — PMU subscribes to this event and reuses
|
||||
// its existing download+install chain (downloadPackageAsync →
|
||||
// installOnePackage). The user sees the row flip to "Installing" while
|
||||
// the download runs, then to "Installed" (or "Failed") when it finishes.
|
||||
bool ok = uninstallResult.value("success", false);
|
||||
if (ok && emitEvent) {
|
||||
LogosMap payload;
|
||||
payload["name"] = packageName;
|
||||
payload["releaseTag"] = releaseTag;
|
||||
payload["mode"] = mode;
|
||||
emitEvent("upgradeUninstallDone", payload.dump());
|
||||
}
|
||||
|
||||
return uninstallResult;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::cancelUpgrade(const std::string& packageName,
|
||||
const std::string& releaseTag)
|
||||
{
|
||||
PendingAction pa;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||
if (m_pendingAction.op != PendingOp::Upgrade
|
||||
|| m_pendingAction.name != packageName
|
||||
|| m_pendingAction.releaseTag != releaseTag) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "No matching pending upgrade for '" + packageName + "'";
|
||||
return response;
|
||||
}
|
||||
// See cancelUninstall for why cancel also requires prior ack.
|
||||
if (!m_pendingAction.acked) {
|
||||
LogosMap response;
|
||||
response["success"] = false;
|
||||
response["error"] = "Pending upgrade for '" + packageName + "' has not been acknowledged";
|
||||
return response;
|
||||
}
|
||||
pa = m_pendingAction;
|
||||
m_pendingAction = {};
|
||||
stopAckTimerLocked();
|
||||
}
|
||||
emitCancellation(pa, "user cancelled");
|
||||
LogosMap response;
|
||||
response["success"] = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
LogosMap PackageManagerImpl::resetPendingAction()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stateMutex);
|
||||
m_pendingAction = {};
|
||||
stopAckTimerLocked();
|
||||
LogosMap response;
|
||||
response["success"] = true;
|
||||
return response;
|
||||
}
|
||||
|
||||
+166
-1
@@ -3,6 +3,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include <cstdint>
|
||||
#include <logos_json.h>
|
||||
|
||||
class PackageManagerLib;
|
||||
@@ -12,6 +16,9 @@ public:
|
||||
PackageManagerImpl();
|
||||
~PackageManagerImpl();
|
||||
|
||||
PackageManagerImpl(const PackageManagerImpl&) = delete;
|
||||
PackageManagerImpl& operator=(const PackageManagerImpl&) = delete;
|
||||
|
||||
// Event callback — wired automatically by the generated glue layer.
|
||||
// Call this to emit named events to other modules / the host application.
|
||||
std::function<void(const std::string& eventName, const std::string& data)> emitEvent;
|
||||
@@ -19,6 +26,16 @@ public:
|
||||
// Install from local LGX file — returns LogosMap {name, path, error, isCoreModule, ...}
|
||||
LogosMap installPlugin(const std::string& pluginPath, bool skipIfNotNewerVersion);
|
||||
|
||||
// Inspect an LGX file without installing. Returns package metadata plus
|
||||
// already-installed status and dependents so callers can show a confirmation
|
||||
// dialog before committing. Shape:
|
||||
// { name, version, type, description, category,
|
||||
// signatureStatus ("signed"|"unsigned"|"invalid"|"error"),
|
||||
// signerDid?, signerName?,
|
||||
// isAlreadyInstalled, installedVersion?,
|
||||
// installedDependents? }
|
||||
LogosMap inspectPackage(const std::string& lgxPath);
|
||||
|
||||
// Directory configuration — embedded (multiple, read-only)
|
||||
void setEmbeddedModulesDirectory(const std::string& dir);
|
||||
void addEmbeddedModulesDirectory(const std::string& dir);
|
||||
@@ -29,11 +46,44 @@ public:
|
||||
void setUserModulesDirectory(const std::string& dir);
|
||||
void setUserUiPluginsDirectory(const std::string& dir);
|
||||
|
||||
// Scanning — each returns LogosList (JSON array with all manifest fields + installDir + mainFilePath)
|
||||
// Scanning — each returns LogosList (JSON array with all manifest fields
|
||||
// + installDir + mainFilePath + installType ("embedded"|"user"))
|
||||
LogosList getInstalledPackages();
|
||||
LogosList getInstalledModules();
|
||||
LogosList getInstalledUiPlugins();
|
||||
|
||||
// Uninstall a user-installed package. Refuses embedded packages.
|
||||
// Returns { success: bool, error?: string, removedFiles?: [string] }.
|
||||
// On success also emits "corePluginUninstalled" or "uiPluginUninstalled".
|
||||
//
|
||||
// This is the ungated path — it performs the uninstall immediately.
|
||||
// Headless callers (lgpm, scripts, logoscore-driven automation) use this
|
||||
// directly. GUI callers should prefer requestUninstall below, which gates
|
||||
// the destructive work behind a listener-driven confirmation dialog.
|
||||
LogosMap uninstallPackage(const std::string& packageName);
|
||||
|
||||
// Forward dependency walk for an installed package. Returns a tree of
|
||||
// { name, status, version, installType, children: [...] } rooted at the
|
||||
// queried package. Stops at NotInstalled/Cycle nodes. `recursive=false`
|
||||
// walks only one level deep (children have empty `children` arrays);
|
||||
// `recursive=true` walks the full tree.
|
||||
LogosMap resolveDependencies(const std::string& packageName, bool recursive);
|
||||
|
||||
// Reverse dependency walk — same tree shape as resolveDependencies but
|
||||
// for the inverse edge. Returns a tree of { name, version, type,
|
||||
// installType, installDir, children: [...] } rooted at the queried
|
||||
// package. `recursive=false` walks only depth-1 (direct reverse
|
||||
// neighbours, with empty `children`); `recursive=true` walks the full
|
||||
// reverse subtree.
|
||||
LogosMap resolveDependents(const std::string& packageName, bool recursive);
|
||||
|
||||
// Flat projections of the two walks above. Each returns a LogosList of
|
||||
// per-node maps (same fields as the tree version minus `children`).
|
||||
// `recursive=false` emits only direct neighbours; `recursive=true`
|
||||
// emits every descendant, BFS-ordered and deduplicated by name.
|
||||
LogosList resolveFlatDependencies(const std::string& packageName, bool recursive);
|
||||
LogosList resolveFlatDependents(const std::string& packageName, bool recursive);
|
||||
|
||||
// Platform variants this build accepts (e.g. ["darwin-arm64-dev"] or ["darwin-arm64"])
|
||||
std::vector<std::string> getValidVariants();
|
||||
|
||||
@@ -50,6 +100,121 @@ public:
|
||||
LogosMap removeTrustedKey(const std::string& name);
|
||||
LogosList listTrustedKeys();
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Gated uninstall / upgrade flow with two-phase listener ack.
|
||||
// ----------------------------------------------------------------
|
||||
//
|
||||
// Protocol (GUI-only — headless callers must use the ungated slots above):
|
||||
//
|
||||
// 1. Caller invokes requestUninstall(name) / requestUpgrade(name, tag, mode).
|
||||
// Returns LogosResult-shaped LogosMap { success, error? } synchronously.
|
||||
// On success, sets pending state, emits "beforeUninstall" / "beforeUpgrade",
|
||||
// and starts a 3s ack-reception timer.
|
||||
//
|
||||
// 2. Any listener handling the event MUST immediately call
|
||||
// ackPendingAction(name). This cancels the ack timer and tells the module
|
||||
// "I'm driving the dialog — wait indefinitely for my decision."
|
||||
//
|
||||
// 3a. If the ack timer fires with no ack, the module clears pending state
|
||||
// and emits "uninstallCancelled" / "upgradeCancelled" with a timeout
|
||||
// reason. Destructive work never runs without an owning listener.
|
||||
//
|
||||
// 3b. Once acked, confirmUninstall / confirmUpgrade performs the work;
|
||||
// cancelUninstall / cancelUpgrade aborts and emits the cancellation
|
||||
// event with reason "user cancelled". Confirm and cancel both
|
||||
// require a prior ack — an un-acked pending state is owned by the
|
||||
// ack-reception timer, and bypassing it would short-circuit the
|
||||
// "no listener acknowledged" timeout path that initiators rely on.
|
||||
// Calling confirm/cancel before ack returns { success: false,
|
||||
// error: "Pending <op> for '<name>' has not been acknowledged" }.
|
||||
//
|
||||
// Only one gated flow can be pending globally (across packages and ops).
|
||||
// A second requestXxx while one is pending returns { success: false, error: ... }.
|
||||
LogosMap requestUninstall(const std::string& packageName);
|
||||
LogosMap requestUpgrade(const std::string& packageName, const std::string& releaseTag, int64_t mode);
|
||||
|
||||
LogosMap ackPendingAction(const std::string& packageName);
|
||||
|
||||
LogosMap confirmUninstall(const std::string& packageName);
|
||||
LogosMap cancelUninstall(const std::string& packageName);
|
||||
LogosMap confirmUpgrade(const std::string& packageName, const std::string& releaseTag);
|
||||
LogosMap cancelUpgrade(const std::string& packageName, const std::string& releaseTag);
|
||||
|
||||
// Belt-and-braces: clears any pending state. Called by Basecamp at startup
|
||||
// so a crash mid-dialog in a previous session doesn't block new requests.
|
||||
LogosMap resetPendingAction();
|
||||
|
||||
// Test-only hook — override the ack-reception timeout so timeout-path
|
||||
// tests complete in milliseconds instead of the production 3-second
|
||||
// default. Must be called before any request*() on this instance (i.e.
|
||||
// while no ack-timer worker is running). Not thread-safe against a
|
||||
// concurrent worker.
|
||||
void setAckTimeoutMsForTest(int ms) { m_ackTimeoutMs = ms; }
|
||||
|
||||
private:
|
||||
enum class PendingOp { None, Uninstall, Upgrade };
|
||||
|
||||
struct PendingAction {
|
||||
PendingOp op = PendingOp::None;
|
||||
std::string name;
|
||||
std::string releaseTag; // upgrade only
|
||||
int64_t mode = 0; // upgrade only (UpgradeMode enum as int)
|
||||
bool acked = false;
|
||||
};
|
||||
|
||||
// Production ack-reception timeout. Overridable per-instance via
|
||||
// setAckTimeoutMsForTest so timeout-path tests don't need to wait
|
||||
// 3 real seconds each.
|
||||
int m_ackTimeoutMs = 3000;
|
||||
static const char* opName(PendingOp op);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Pure-C++ ack timer.
|
||||
// ----------------------------------------------------------------
|
||||
//
|
||||
// When a gated operation (requestUninstall / requestUpgrade) is initiated,
|
||||
// a worker thread is spawned that waits on m_ackCv for up to m_ackTimeoutMs.
|
||||
// Any concurrent state change — ackPendingAction, confirmXxx, cancelXxx,
|
||||
// resetPendingAction, a new requestXxx, or the destructor — bumps
|
||||
// m_ackGeneration and notifies the CV. The worker wakes up, observes that
|
||||
// its captured generation no longer matches the current one (or m_ackShutdown
|
||||
// is set), and exits silently. Only a worker whose captured generation still
|
||||
// matches after a full timeout proceeds to emit the cancellation event.
|
||||
//
|
||||
// Threading rules:
|
||||
// - startAckTimerLocked / stopAckTimerLocked must be called with
|
||||
// m_stateMutex held.
|
||||
// - startAckTimerLocked temporarily releases the lock while joining the
|
||||
// previous worker thread (if any), to avoid a deadlock where the
|
||||
// worker can't exit its wait_for because we're holding the lock.
|
||||
// - stopAckTimerLocked never joins — it only signals. The worker exits
|
||||
// on its own; the next startAckTimerLocked (or the destructor) joins
|
||||
// the now-finished thread.
|
||||
// - The destructor sets m_ackShutdown, notifies the CV, and joins the
|
||||
// worker before destroying any other state.
|
||||
//
|
||||
// Everything else on this class (m_lib access, file scanning, emitEvent
|
||||
// from user code) runs serially on the module thread via the glue layer's
|
||||
// queued connection, so no additional locking is needed beyond
|
||||
// m_stateMutex guarding the pending-action state.
|
||||
void startAckTimerLocked(std::unique_lock<std::mutex>& lock);
|
||||
void stopAckTimerLocked();
|
||||
void ackTimerWorker(uint64_t myGeneration);
|
||||
|
||||
// Helpers used by the gated-flow slots.
|
||||
bool isEmbedded(const std::string& packageName) const;
|
||||
std::vector<std::string> installedDependentsNames(const std::string& packageName) const;
|
||||
LogosMap doUninstall(const std::string& packageName);
|
||||
void emitCancellation(const PendingAction& pa, const std::string& reason);
|
||||
|
||||
PackageManagerLib* m_lib;
|
||||
|
||||
// Guards m_pendingAction and the ack-timer generation/shutdown flags.
|
||||
mutable std::mutex m_stateMutex;
|
||||
std::condition_variable m_ackCv;
|
||||
std::thread m_ackThread;
|
||||
uint64_t m_ackGeneration = 0;
|
||||
bool m_ackShutdown = false;
|
||||
|
||||
PendingAction m_pendingAction;
|
||||
};
|
||||
|
||||
@@ -46,4 +46,61 @@ void lgx_free_keyring_list(lgx_keyring_list_t list) {
|
||||
(void)list;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Package loading / inspection — unit tests never invoke inspectPackage, but
|
||||
// the symbols must exist at link time because package_manager_impl.cpp
|
||||
// references them unconditionally. Stubs return benign zeroes/nulls.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
lgx_package_t lgx_load(const char* path) {
|
||||
LOGOS_CMOCK_RECORD("lgx_load");
|
||||
(void)path;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void lgx_free_package(lgx_package_t pkg) {
|
||||
LOGOS_CMOCK_RECORD("lgx_free_package");
|
||||
(void)pkg;
|
||||
}
|
||||
|
||||
const char* lgx_get_last_error(void) {
|
||||
LOGOS_CMOCK_RECORD("lgx_get_last_error");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* lgx_get_name(lgx_package_t pkg) {
|
||||
LOGOS_CMOCK_RECORD("lgx_get_name");
|
||||
(void)pkg;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* lgx_get_version(lgx_package_t pkg) {
|
||||
LOGOS_CMOCK_RECORD("lgx_get_version");
|
||||
(void)pkg;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* lgx_get_description(lgx_package_t pkg) {
|
||||
LOGOS_CMOCK_RECORD("lgx_get_description");
|
||||
(void)pkg;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* lgx_get_manifest_json(lgx_package_t pkg) {
|
||||
LOGOS_CMOCK_RECORD("lgx_get_manifest_json");
|
||||
(void)pkg;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char** lgx_get_variants(lgx_package_t pkg) {
|
||||
LOGOS_CMOCK_RECORD("lgx_get_variants");
|
||||
(void)pkg;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void lgx_free_string_array(const char** array) {
|
||||
LOGOS_CMOCK_RECORD("lgx_free_string_array");
|
||||
(void)array;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -1,17 +1,102 @@
|
||||
// Mock PackageManagerLib for package_manager_module unit tests (link-time substitution).
|
||||
//
|
||||
// Everything the impl reads back from the library is registered via the
|
||||
// setMock*() helpers in mock_package_manager_lib.h. Registries are file-static
|
||||
// because each struct type carries std::string / std::vector members and
|
||||
// can't go through LogosCMockStore's memcpy-based return slot.
|
||||
//
|
||||
// State reset: LogosTestContext constructor calls LogosCMockStore::reset(),
|
||||
// which zeroes every recorded call. We hook into that by checking a sentinel
|
||||
// call count on entry to every mock — count == 0 means the store was just
|
||||
// reset, so we also clear our struct registries. Tests can then set up
|
||||
// registries AFTER constructing LogosTestContext with the usual pattern.
|
||||
|
||||
#include <logos_clib_mock.h>
|
||||
#include <package_manager_lib.h>
|
||||
|
||||
static std::string mockDupCStr(const char* key, const char* fallback) {
|
||||
#include "mock_package_manager_lib.h"
|
||||
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File-static registries populated by setMock*()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<InstalledPackage> s_installedPackages;
|
||||
std::vector<InstalledPackage> s_installedModules;
|
||||
std::vector<InstalledPackage> s_installedUiPlugins;
|
||||
std::optional<DependencyTreeNode> s_dependencyTree;
|
||||
std::optional<DependentTreeNode> s_dependentTree;
|
||||
|
||||
// Sentinel key recorded on the first mock interaction per-test. When
|
||||
// LogosCMockStore::reset() zeroes all call counts (inside LogosTestContext
|
||||
// ctor), the sentinel disappears too — the next mock call sees count == 0,
|
||||
// wipes the struct registries, and re-arms the sentinel.
|
||||
constexpr const char* kResetSentinel = "__pm_mock_reset_sentinel__";
|
||||
|
||||
void ensureFreshStateForTest() {
|
||||
auto& store = LogosCMockStore::instance();
|
||||
if (store.callCount(kResetSentinel) == 0) {
|
||||
s_installedPackages.clear();
|
||||
s_installedModules.clear();
|
||||
s_installedUiPlugins.clear();
|
||||
s_dependencyTree.reset();
|
||||
s_dependentTree.reset();
|
||||
store.recordCall(kResetSentinel);
|
||||
}
|
||||
}
|
||||
|
||||
std::string mockDupCStr(const char* key, const char* fallback) {
|
||||
const char* ret = LOGOS_CMOCK_RETURN_STRING(key);
|
||||
if (ret && ret[0])
|
||||
return std::string(ret);
|
||||
return fallback ? std::string(fallback) : std::string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setters (exposed via mock_package_manager_lib.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void setMockInstalledPackages(std::vector<InstalledPackage> v) {
|
||||
ensureFreshStateForTest();
|
||||
s_installedPackages = std::move(v);
|
||||
}
|
||||
|
||||
void setMockInstalledModules(std::vector<InstalledPackage> v) {
|
||||
ensureFreshStateForTest();
|
||||
s_installedModules = std::move(v);
|
||||
}
|
||||
|
||||
void setMockInstalledUiPlugins(std::vector<InstalledPackage> v) {
|
||||
ensureFreshStateForTest();
|
||||
s_installedUiPlugins = std::move(v);
|
||||
}
|
||||
|
||||
void setMockDependencyTree(std::optional<DependencyTreeNode> tree) {
|
||||
ensureFreshStateForTest();
|
||||
s_dependencyTree = std::move(tree);
|
||||
}
|
||||
|
||||
void setMockDependentTree(std::optional<DependentTreeNode> tree) {
|
||||
ensureFreshStateForTest();
|
||||
s_dependentTree = std::move(tree);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PackageManagerLib method impls
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
PackageManagerLib::PackageManagerLib() {
|
||||
LOGOS_CMOCK_RECORD("PackageManagerLib_ctor");
|
||||
ensureFreshStateForTest();
|
||||
}
|
||||
|
||||
PackageManagerLib::~PackageManagerLib() {
|
||||
@@ -72,19 +157,22 @@ std::string PackageManagerLib::installPluginFile(const std::string& pluginPath,
|
||||
return mockDupCStr("installPluginFile_result", "");
|
||||
}
|
||||
|
||||
std::string PackageManagerLib::getInstalledPackages() {
|
||||
std::vector<InstalledPackage> PackageManagerLib::getInstalledPackages() {
|
||||
LOGOS_CMOCK_RECORD("getInstalledPackages");
|
||||
return mockDupCStr("getInstalledPackages", "[]");
|
||||
ensureFreshStateForTest();
|
||||
return s_installedPackages;
|
||||
}
|
||||
|
||||
std::string PackageManagerLib::getInstalledModules() {
|
||||
std::vector<InstalledPackage> PackageManagerLib::getInstalledModules() {
|
||||
LOGOS_CMOCK_RECORD("getInstalledModules");
|
||||
return mockDupCStr("getInstalledModules", "[]");
|
||||
ensureFreshStateForTest();
|
||||
return s_installedModules;
|
||||
}
|
||||
|
||||
std::string PackageManagerLib::getInstalledUiPlugins() {
|
||||
std::vector<InstalledPackage> PackageManagerLib::getInstalledUiPlugins() {
|
||||
LOGOS_CMOCK_RECORD("getInstalledUiPlugins");
|
||||
return mockDupCStr("getInstalledUiPlugins", "[]");
|
||||
ensureFreshStateForTest();
|
||||
return s_installedUiPlugins;
|
||||
}
|
||||
|
||||
std::vector<std::string> PackageManagerLib::platformVariantsToTry() {
|
||||
@@ -111,6 +199,91 @@ std::string PackageManagerLib::keyringDirectory() {
|
||||
return mockDupCStr("keyringDirectory", "");
|
||||
}
|
||||
|
||||
UninstallResult PackageManagerLib::uninstallPackage(const std::string& packageName) {
|
||||
LOGOS_CMOCK_RECORD("uninstallPackage");
|
||||
(void)packageName;
|
||||
UninstallResult r;
|
||||
r.success = LOGOS_CMOCK_RETURN(bool, "uninstallPackage_success");
|
||||
const char* err = LOGOS_CMOCK_RETURN_STRING("uninstallPackage_error");
|
||||
if (err && err[0]) {
|
||||
r.errorMsg = err;
|
||||
}
|
||||
const char* removed = LOGOS_CMOCK_RETURN_STRING("uninstallPackage_removed");
|
||||
if (removed && removed[0]) {
|
||||
// Comma-separated list for convenience
|
||||
std::string s(removed);
|
||||
std::string::size_type pos = 0, next;
|
||||
while ((next = s.find(',', pos)) != std::string::npos) {
|
||||
r.removedFiles.emplace_back(s.substr(pos, next - pos));
|
||||
pos = next + 1;
|
||||
}
|
||||
r.removedFiles.emplace_back(s.substr(pos));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
std::optional<DependencyTreeNode> PackageManagerLib::resolveDependencies(const std::string& packageName) {
|
||||
LOGOS_CMOCK_RECORD("resolveDependencies");
|
||||
(void)packageName;
|
||||
ensureFreshStateForTest();
|
||||
return s_dependencyTree;
|
||||
}
|
||||
|
||||
std::optional<DependentTreeNode> PackageManagerLib::resolveDependents(const std::string& packageName) {
|
||||
LOGOS_CMOCK_RECORD("resolveDependents");
|
||||
(void)packageName;
|
||||
ensureFreshStateForTest();
|
||||
return s_dependentTree;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// flatten() — hand-rolled copies of the real-lib implementations. The stub
|
||||
// header mirrors the real header's declarations, so the tree structs expose
|
||||
// the same member function; the mock provides bodies so link-time tests can
|
||||
// exercise the same BFS-dedup behaviour without linking the real lib.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<DependencyTreeNode> DependencyTreeNode::flatten() const {
|
||||
std::vector<DependencyTreeNode> out;
|
||||
std::unordered_set<std::string> seen;
|
||||
std::deque<const DependencyTreeNode*> queue;
|
||||
for (const auto& c : children) queue.push_back(&c);
|
||||
while (!queue.empty()) {
|
||||
const DependencyTreeNode* n = queue.front();
|
||||
queue.pop_front();
|
||||
if (!seen.insert(n->name).second) continue;
|
||||
DependencyTreeNode copy;
|
||||
copy.name = n->name;
|
||||
copy.status = n->status;
|
||||
copy.version = n->version;
|
||||
copy.installType = n->installType;
|
||||
out.push_back(std::move(copy));
|
||||
for (const auto& c : n->children) queue.push_back(&c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<DependentTreeNode> DependentTreeNode::flatten() const {
|
||||
std::vector<DependentTreeNode> out;
|
||||
std::unordered_set<std::string> seen;
|
||||
std::deque<const DependentTreeNode*> queue;
|
||||
for (const auto& c : children) queue.push_back(&c);
|
||||
while (!queue.empty()) {
|
||||
const DependentTreeNode* n = queue.front();
|
||||
queue.pop_front();
|
||||
if (!seen.insert(n->name).second) continue;
|
||||
DependentTreeNode copy;
|
||||
copy.name = n->name;
|
||||
copy.version = n->version;
|
||||
copy.type = n->type;
|
||||
copy.installType = n->installType;
|
||||
copy.installDir = n->installDir;
|
||||
out.push_back(std::move(copy));
|
||||
for (const auto& c : n->children) queue.push_back(&c);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
SignatureVerificationResult PackageManagerLib::verifyPackageSignature(const std::string& lgxPath) {
|
||||
LOGOS_CMOCK_RECORD("verifyPackageSignature");
|
||||
(void)lgxPath;
|
||||
@@ -140,3 +313,26 @@ SignatureVerificationResult PackageManagerLib::verifyPackageSignature(const std:
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// installTypeToString / dependencyStatusToString are declared in
|
||||
// package_manager_lib.h but referenced at link time by any JSON helper that
|
||||
// serialises InstalledPackage / DependencyTreeNode. The real library supplies
|
||||
// them; in unit tests they're not exercised because PackageManagerImpl builds
|
||||
// LogosMap directly from the structs without touching them, but providing a
|
||||
// stub keeps the mock self-contained should a future test need it.
|
||||
const char* installTypeToString(InstallType t) {
|
||||
switch (t) {
|
||||
case InstallType::Embedded: return "embedded";
|
||||
case InstallType::User: return "user";
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
|
||||
const char* dependencyStatusToString(DependencyStatus s) {
|
||||
switch (s) {
|
||||
case DependencyStatus::Installed: return "installed";
|
||||
case DependencyStatus::NotInstalled: return "not_installed";
|
||||
case DependencyStatus::Cycle: return "cycle";
|
||||
}
|
||||
return "not_installed";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
// Test-side helpers that let tests register the structs the mocked
|
||||
// PackageManagerLib should return. The mock implementation lives in
|
||||
// mock_package_manager_lib.cpp and is wired in via the logos_test() CMake
|
||||
// function (MOCK_C_SOURCES).
|
||||
//
|
||||
// Why a struct registry instead of `LogosCMockStore` returns?
|
||||
// - LogosCMockStore uses `returnsRaw` + memcpy which is only safe for
|
||||
// trivially-copyable types. Our struct returns carry std::string and
|
||||
// std::vector, so memcpy would be undefined behaviour.
|
||||
// - Recursive DependencyTreeNode would need a bespoke serialisation; a
|
||||
// file-static std::optional<DependencyTreeNode> sidesteps that.
|
||||
//
|
||||
// Registries reset automatically on each new LogosTestContext. The mock
|
||||
// piggybacks on LogosCMockStore::reset() via a sentinel call so tests can
|
||||
// be written exactly like the old JSON-returning pattern — construct a
|
||||
// context, set the mocks, instantiate the impl.
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "package_manager_lib.h"
|
||||
|
||||
void setMockInstalledPackages(std::vector<InstalledPackage> v);
|
||||
void setMockInstalledModules(std::vector<InstalledPackage> v);
|
||||
void setMockInstalledUiPlugins(std::vector<InstalledPackage> v);
|
||||
void setMockDependencyTree(std::optional<DependencyTreeNode> tree);
|
||||
void setMockDependentTree(std::optional<DependentTreeNode> tree);
|
||||
@@ -30,6 +30,11 @@ typedef struct {
|
||||
size_t count;
|
||||
} lgx_keyring_list_t;
|
||||
|
||||
/* Opaque package handle — used by inspectPackage(). Real layout lives in
|
||||
* ../lib; unit tests never invoke these functions (they're only referenced
|
||||
* from code paths that aren't exercised here), so a forward decl is enough. */
|
||||
typedef struct lgx_package_opaque* lgx_package_t;
|
||||
|
||||
lgx_result_t lgx_keyring_add(const char* keyring_dir,
|
||||
const char* name,
|
||||
const char* did,
|
||||
@@ -42,6 +47,19 @@ lgx_keyring_list_t lgx_keyring_list(const char* keyring_dir);
|
||||
|
||||
void lgx_free_keyring_list(lgx_keyring_list_t list);
|
||||
|
||||
/* Package loading / inspection — only declarations needed for unit-test
|
||||
* compilation; the real implementations are provided by the lgx library
|
||||
* at link time (integration tests) or stubbed out when unexercised. */
|
||||
lgx_package_t lgx_load(const char* path);
|
||||
void lgx_free_package(lgx_package_t pkg);
|
||||
const char* lgx_get_last_error(void);
|
||||
const char* lgx_get_name(lgx_package_t pkg);
|
||||
const char* lgx_get_version(lgx_package_t pkg);
|
||||
const char* lgx_get_description(lgx_package_t pkg);
|
||||
const char* lgx_get_manifest_json(lgx_package_t pkg);
|
||||
const char** lgx_get_variants(lgx_package_t pkg);
|
||||
void lgx_free_string_array(const char** array);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -12,6 +14,17 @@ enum class SignaturePolicy {
|
||||
REQUIRE
|
||||
};
|
||||
|
||||
enum class InstallType {
|
||||
Embedded,
|
||||
User,
|
||||
};
|
||||
|
||||
enum class DependencyStatus {
|
||||
Installed,
|
||||
NotInstalled,
|
||||
Cycle,
|
||||
};
|
||||
|
||||
struct SignatureVerificationResult {
|
||||
bool is_signed = false;
|
||||
bool signature_valid = false;
|
||||
@@ -23,6 +36,65 @@ struct SignatureVerificationResult {
|
||||
std::string error;
|
||||
};
|
||||
|
||||
struct UninstallResult {
|
||||
bool success = false;
|
||||
std::string errorMsg;
|
||||
std::vector<std::string> removedFiles;
|
||||
};
|
||||
|
||||
// Mirrors the real lib's Hashes struct. Only `root` is read today but keeping
|
||||
// the nested shape matches manifest.json and leaves room for additions.
|
||||
struct Hashes {
|
||||
std::string root;
|
||||
};
|
||||
|
||||
// Mirrors InstalledPackage in package_manager_lib.h.
|
||||
struct InstalledPackage {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string description;
|
||||
std::string type;
|
||||
std::string category;
|
||||
std::string author;
|
||||
std::string license;
|
||||
std::string icon;
|
||||
std::string view;
|
||||
std::vector<std::string> dependencies;
|
||||
Hashes hashes;
|
||||
InstallType installType = InstallType::User;
|
||||
std::string installDir;
|
||||
std::string mainFilePath;
|
||||
};
|
||||
|
||||
// Mirrors DependencyTreeNode in package_manager_lib.h.
|
||||
struct DependencyTreeNode {
|
||||
std::string name;
|
||||
DependencyStatus status = DependencyStatus::NotInstalled;
|
||||
std::string version;
|
||||
InstallType installType = InstallType::User;
|
||||
std::vector<DependencyTreeNode> children;
|
||||
|
||||
// Matches the real lib — descendants-only BFS, name-deduped, children
|
||||
// cleared on returned copies. Implementation lives in the mock .cpp so
|
||||
// the stub header stays declaration-only.
|
||||
std::vector<DependencyTreeNode> flatten() const;
|
||||
};
|
||||
|
||||
// Mirrors DependentTreeNode in package_manager_lib.h.
|
||||
struct DependentTreeNode {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string type;
|
||||
InstallType installType = InstallType::User;
|
||||
std::string installDir;
|
||||
std::vector<DependentTreeNode> children;
|
||||
|
||||
std::vector<DependentTreeNode> flatten() const;
|
||||
};
|
||||
|
||||
const char* installTypeToString(InstallType t);
|
||||
const char* dependencyStatusToString(DependencyStatus s);
|
||||
|
||||
class PackageManagerLib {
|
||||
public:
|
||||
PackageManagerLib();
|
||||
@@ -40,9 +112,9 @@ public:
|
||||
std::string* installedPluginPath = nullptr,
|
||||
bool* isCoreModule = nullptr);
|
||||
|
||||
std::string getInstalledPackages();
|
||||
std::string getInstalledModules();
|
||||
std::string getInstalledUiPlugins();
|
||||
std::vector<InstalledPackage> getInstalledPackages();
|
||||
std::vector<InstalledPackage> getInstalledModules();
|
||||
std::vector<InstalledPackage> getInstalledUiPlugins();
|
||||
|
||||
static std::vector<std::string> platformVariantsToTry();
|
||||
|
||||
@@ -51,4 +123,9 @@ public:
|
||||
std::string keyringDirectory();
|
||||
|
||||
SignatureVerificationResult verifyPackageSignature(const std::string& lgxPath);
|
||||
|
||||
UninstallResult uninstallPackage(const std::string& packageName);
|
||||
|
||||
std::optional<DependencyTreeNode> resolveDependencies(const std::string& packageName);
|
||||
std::optional<DependentTreeNode> resolveDependents(const std::string& packageName);
|
||||
};
|
||||
|
||||
+1060
-10
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user