Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca5290015a | ||
|
|
ad0c00bade | ||
|
|
4db5a7a132 | ||
|
|
c567b229cc |
@@ -6,3 +6,4 @@ smoke-platform
|
|||||||
plugins/
|
plugins/
|
||||||
vendor/
|
vendor/
|
||||||
frontend/e2e-results/
|
frontend/e2e-results/
|
||||||
|
release/
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Verstak Desktop
|
||||||
|
Copyright (C) 2026 Verstak contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
The complete license text is available at:
|
||||||
|
https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
@@ -1,3 +1,81 @@
|
|||||||
# verstak-desktop
|
# Verstak Desktop
|
||||||
|
|
||||||
Verstak Core Platform + UI Shell — plugin runtime, capability registry, vault API, event bus, sync client
|
Verstak is a local-first desktop workspace for files, notes, browser captures,
|
||||||
|
activity and work journal entries. This repository contains the Go/Wails desktop
|
||||||
|
host and UI shell; user-facing functions are delivered by official plugins.
|
||||||
|
|
||||||
|
> **Alpha software.** Use a disposable vault while evaluating it. APIs, storage
|
||||||
|
> formats and packaging can change before the first stable release.
|
||||||
|
|
||||||
|
## What the alpha does
|
||||||
|
|
||||||
|
- keeps each workspace's identity independent of its current directory name;
|
||||||
|
- keeps browser captures in a reviewable Inbox, with archive and restore rather
|
||||||
|
than a destructive “remove from inbox” action;
|
||||||
|
- records local file/note activity and, after review, turns it into journal
|
||||||
|
entries without creating a workspace automatically;
|
||||||
|
- accepts optional browser domain-time batches. The browser extension is
|
||||||
|
opt-in: it sends only normalized domain names and bounded durations, never
|
||||||
|
URLs, page titles, page contents, keystrokes or navigation history;
|
||||||
|
- runs official plugins from a `plugins/` directory next to the executable.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
Clone these repositories as siblings when building a complete local setup:
|
||||||
|
|
||||||
|
| Repository | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `verstak-desktop` | desktop host and UI shell |
|
||||||
|
| `verstak-official-plugins` | Files, Notes, Browser Inbox, Activity, Journal and other official plugins |
|
||||||
|
| `verstak-browser-extension` | Chromium/Firefox capture and optional domain-activity extension |
|
||||||
|
| `verstak-sdk` | plugin manifest schema and TypeScript API |
|
||||||
|
|
||||||
|
## Build from source (Linux)
|
||||||
|
|
||||||
|
Requirements: Go, Node.js with npm, Python 3, the [Wails v2 build
|
||||||
|
prerequisites](https://wails.io/docs/gettingstarted/installation/), and the
|
||||||
|
WebKitGTK development package for your distribution.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.mirv.top/verstak/verstak-sdk.git
|
||||||
|
git clone https://git.mirv.top/verstak/verstak-official-plugins.git
|
||||||
|
git clone https://git.mirv.top/verstak/verstak-desktop.git
|
||||||
|
git clone https://git.mirv.top/verstak/verstak-browser-extension.git
|
||||||
|
|
||||||
|
cd verstak-sdk && ./scripts/build.sh
|
||||||
|
cd ../verstak-official-plugins && ./scripts/build.sh
|
||||||
|
cd ../verstak-desktop
|
||||||
|
./scripts/install-dev-plugins.sh
|
||||||
|
./scripts/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The Linux executable is placed in `build/bin/`. Start it with `--debug` to
|
||||||
|
display internal plugin-provider IDs and write diagnostic logs.
|
||||||
|
|
||||||
|
## Release artifacts
|
||||||
|
|
||||||
|
Maintainers can create a Linux desktop tarball after the sibling repositories
|
||||||
|
above have been built:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd verstak-desktop
|
||||||
|
./scripts/release.sh v0.1.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
It writes `release/verstak-desktop-linux-amd64-<version>.tar.gz` and a matching
|
||||||
|
`SHA256SUMS` file. The archive is self-contained: unpack it and run the
|
||||||
|
included executable. Browser and SDK packages have their own release scripts
|
||||||
|
in their repositories.
|
||||||
|
|
||||||
|
## Privacy and activity tracking
|
||||||
|
|
||||||
|
The extension's passive domain tracker is disabled by default. Enabling it
|
||||||
|
requires an explicit choice in the extension and lets the user maintain a
|
||||||
|
domain exclusion list. Manual page, selection and link captures are separate
|
||||||
|
actions; they enter Browser Inbox and never create a workspace or journal
|
||||||
|
entry by themselves.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2026 Verstak contributors. Licensed under
|
||||||
|
[GNU AGPLv3 or later](LICENSE).
|
||||||
|
|||||||
@@ -219,6 +219,7 @@
|
|||||||
const settings = await App.GetAppSettings();
|
const settings = await App.GetAppSettings();
|
||||||
debug.log('[App] checkVault: GetAppSettings returned', settings);
|
debug.log('[App] checkVault: GetAppSettings returned', settings);
|
||||||
flog('checkVault: GetAppSettings returned');
|
flog('checkVault: GetAppSettings returned');
|
||||||
|
if (settings?.debug) debug.enable();
|
||||||
|
|
||||||
debug.log('[App] checkVault: calling GetVaultStatus...');
|
debug.log('[App] checkVault: calling GetVaultStatus...');
|
||||||
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
|
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
|
||||||
|
|||||||
@@ -136,6 +136,7 @@
|
|||||||
].flatMap(key => normalizeRows(settings?.[key])).filter(item => {
|
].flatMap(key => normalizeRows(settings?.[key])).filter(item => {
|
||||||
const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
|
const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
|
||||||
if (tagged !== workspace) return false;
|
if (tagged !== workspace) return false;
|
||||||
|
if (String(item.globalState || 'inbox') === 'archived') return false;
|
||||||
const captureId = String(item.captureId || '');
|
const captureId = String(item.captureId || '');
|
||||||
if (!captureId || seen.has(captureId)) return !captureId;
|
if (!captureId || seen.has(captureId)) return !captureId;
|
||||||
seen.add(captureId);
|
seen.add(captureId);
|
||||||
@@ -501,9 +502,14 @@
|
|||||||
const workspaceAtStart = String(workspaceRootPath || '').trim();
|
const workspaceAtStart = String(workspaceRootPath || '').trim();
|
||||||
loadedWorkspaceRoot = workspaceAtStart;
|
loadedWorkspaceRoot = workspaceAtStart;
|
||||||
loading = true;
|
loading = true;
|
||||||
const [browserSettings, activitySettings, journalSettings, todoSettings, resources] = await Promise.all([
|
const [browserSettings, activitySettings, activityRecords, journalSettings, todoSettings, resources] = await Promise.all([
|
||||||
readPluginSettings('verstak.browser-inbox'),
|
readPluginSettings('verstak.browser-inbox'),
|
||||||
readPluginSettings('verstak.activity'),
|
readPluginSettings('verstak.activity'),
|
||||||
|
App.ReadPluginDataNDJSON
|
||||||
|
? App.ReadPluginDataNDJSON('verstak.activity', 'activity-events')
|
||||||
|
.then(value => decodeTuple(value, []))
|
||||||
|
.catch(() => [])
|
||||||
|
: Promise.resolve([]),
|
||||||
readPluginSettings('verstak.journal'),
|
readPluginSettings('verstak.journal'),
|
||||||
readPluginSettings('verstak.todo'),
|
readPluginSettings('verstak.todo'),
|
||||||
loadWorkspaceResources(),
|
loadWorkspaceResources(),
|
||||||
@@ -511,11 +517,10 @@
|
|||||||
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
||||||
|
|
||||||
captures = browserCaptureRowsForWorkspace(browserSettings);
|
captures = browserCaptureRowsForWorkspace(browserSettings);
|
||||||
activityEvents = rowsFor(activitySettings, [
|
activityEvents = normalizeRows(activityRecords).filter(item => {
|
||||||
workspaceKey('events:workspace:'),
|
const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
|
||||||
'events:global',
|
return !tagged || tagged === workspaceAtStart;
|
||||||
'events',
|
});
|
||||||
]);
|
|
||||||
journalEntries = rowsFor(journalSettings, [
|
journalEntries = rowsFor(journalSettings, [
|
||||||
workspaceKey('worklog:workspace:'),
|
workspaceKey('worklog:workspace:'),
|
||||||
'worklog',
|
'worklog',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
|
||||||
import Icon from '../ui/Icon.svelte';
|
import Icon from '../ui/Icon.svelte';
|
||||||
import { i18n } from '../i18n/index.js';
|
import { i18n } from '../i18n/index.js';
|
||||||
|
import { debug } from '../log/debug.js';
|
||||||
|
|
||||||
export let openedResource = null;
|
export let openedResource = null;
|
||||||
let locale = i18n.getLocale();
|
let locale = i18n.getLocale();
|
||||||
@@ -53,7 +54,9 @@
|
|||||||
{:else if openedResource}
|
{:else if openedResource}
|
||||||
<div class="workbench-header vt-page-header">
|
<div class="workbench-header vt-page-header">
|
||||||
<span class="workbench-title vt-page-title">{resourcePath}</span>
|
<span class="workbench-title vt-page-title">{resourcePath}</span>
|
||||||
<span class="workbench-provider vt-badge accent">{providerId}</span>
|
{#if debug.isEnabled() && providerId}
|
||||||
|
<span class="workbench-provider vt-badge accent" data-debug-provider>{providerId}</span>
|
||||||
|
{/if}
|
||||||
<button class="close-btn btn-ghost btn-icon" type="button" title={tr('common.close')} aria-label={tr('common.close')} on:click={closeWorkbench}>
|
<button class="close-btn btn-ghost btn-icon" type="button" title={tr('common.close')} aria-label={tr('common.close')} on:click={closeWorkbench}>
|
||||||
<Icon name="x" size={18} />
|
<Icon name="x" size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3343,6 +3343,14 @@
|
|||||||
},
|
},
|
||||||
ReadPluginDataNDJSON: function (pluginId, name) {
|
ReadPluginDataNDJSON: function (pluginId, name) {
|
||||||
var data = (pluginData[pluginId] && pluginData[pluginId][name]) || [];
|
var data = (pluginData[pluginId] && pluginData[pluginId][name]) || [];
|
||||||
|
if (!Array.isArray(data) && pluginId === 'verstak.activity' && name === 'activity-events') data = [];
|
||||||
|
if (!data.length && pluginId === 'verstak.activity' && name === 'activity-events') {
|
||||||
|
data = Object.keys(pluginSettings[pluginId] || {}).filter(function (key) {
|
||||||
|
return key === 'events' || key === 'events:global' || key.indexOf('events:workspace:') === 0;
|
||||||
|
}).flatMap(function (key) {
|
||||||
|
return Array.isArray(pluginSettings[pluginId][key]) ? pluginSettings[pluginId][key] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
return Promise.resolve([Array.isArray(data) ? data.slice() : [], '']);
|
return Promise.resolve([Array.isArray(data) ? data.slice() : [], '']);
|
||||||
},
|
},
|
||||||
WritePluginDataJSON: function (pluginId, name, data) {
|
WritePluginDataJSON: function (pluginId, name, data) {
|
||||||
|
|||||||
@@ -2067,6 +2067,7 @@ func (a *App) GetAppSettings() map[string]interface{} {
|
|||||||
"theme": cfg.Theme,
|
"theme": cfg.Theme,
|
||||||
"language": cfg.Language,
|
"language": cfg.Language,
|
||||||
"devMode": cfg.DevMode,
|
"devMode": cfg.DevMode,
|
||||||
|
"debug": a.debug,
|
||||||
"userPluginsDir": cfg.UserPluginsDir,
|
"userPluginsDir": cfg.UserPluginsDir,
|
||||||
"lastOpenedAt": cfg.LastOpenedAt,
|
"lastOpenedAt": cfg.LastOpenedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1791,11 +1791,14 @@ func TestAppSettingsLanguageAPI(t *testing.T) {
|
|||||||
if err := settings.Update(&appsettings.Config{Theme: "light", DevMode: true}); err != nil {
|
if err := settings.Update(&appsettings.Config{Theme: "light", DevMode: true}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
app := &App{appSettings: settings}
|
app := &App{appSettings: settings, debug: true}
|
||||||
|
|
||||||
if got := app.GetAppSettings()["language"]; got != "system" {
|
if got := app.GetAppSettings()["language"]; got != "system" {
|
||||||
t.Fatalf("initial language = %#v, want system", got)
|
t.Fatalf("initial language = %#v, want system", got)
|
||||||
}
|
}
|
||||||
|
if got := app.GetAppSettings()["debug"]; got != true {
|
||||||
|
t.Fatalf("debug = %#v, want true", got)
|
||||||
|
}
|
||||||
if errStr := app.UpdateAppSettings(map[string]interface{}{"language": "ru"}); errStr != "" {
|
if errStr := app.UpdateAppSettings(map[string]interface{}{"language": "ru"}); errStr != "" {
|
||||||
t.Fatalf("UpdateAppSettings language: %s", errStr)
|
t.Fatalf("UpdateAppSettings language: %s", errStr)
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
VERSION="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" || ! "$VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
|
||||||
|
echo "usage: $0 <version>" >&2
|
||||||
|
echo "example: $0 v0.1.0-alpha.1" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pkg-config >/dev/null || \
|
||||||
|
(! pkg-config --exists webkit2gtk-4.1 && ! pkg-config --exists webkit2gtk-4.0); then
|
||||||
|
echo "Linux desktop releases require libwebkit2gtk-4.1-dev or libwebkit2gtk-4.0-dev." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OFFICIAL_PLUGINS="${VERSTAK_OFFICIAL_PLUGINS_DIR:-$ROOT/../verstak-official-plugins}"
|
||||||
|
if [[ ! -d "$OFFICIAL_PLUGINS" ]]; then
|
||||||
|
echo "official plugins repository not found: $OFFICIAL_PLUGINS" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== verstak desktop release $VERSION ==="
|
||||||
|
(cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh)
|
||||||
|
"$ROOT/scripts/install-dev-plugins.sh"
|
||||||
|
"$ROOT/scripts/build.sh"
|
||||||
|
|
||||||
|
BINARY="$(find "$ROOT/build/bin" -maxdepth 1 -type f -name 'verstak-desktop*' -print -quit)"
|
||||||
|
if [[ -z "$BINARY" ]]; then
|
||||||
|
echo "desktop binary was not produced in build/bin" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_ROOT="$ROOT/release"
|
||||||
|
STAGING="$RELEASE_ROOT/verstak-desktop-$VERSION-linux-amd64"
|
||||||
|
ARCHIVE="$RELEASE_ROOT/verstak-desktop-linux-amd64-$VERSION.tar.gz"
|
||||||
|
rm -rf "$STAGING" "$ARCHIVE"
|
||||||
|
mkdir -p "$STAGING"
|
||||||
|
|
||||||
|
cp "$BINARY" "$STAGING/verstak-desktop"
|
||||||
|
cp "$ROOT/README.md" "$ROOT/LICENSE" "$STAGING/"
|
||||||
|
cp -R "$ROOT/plugins" "$STAGING/plugins"
|
||||||
|
tar -C "$RELEASE_ROOT" -czf "$ARCHIVE" "$(basename "$STAGING")"
|
||||||
|
(cd "$RELEASE_ROOT" && sha256sum "$(basename "$ARCHIVE")" > SHA256SUMS)
|
||||||
|
|
||||||
|
echo "release archive: $ARCHIVE"
|
||||||
|
echo "checksums: $RELEASE_ROOT/SHA256SUMS"
|
||||||
Reference in New Issue
Block a user