aidevops 3.29.8 → 3.29.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-web/src/InventorySurfaces.tsx +215 -42
- package/packages/gui-web/src/styles.css +131 -9
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
58
58
|
[](https://github.com/marcusquinn)
|
|
59
59
|
|
|
60
60
|
<!-- Release & Version Info -->
|
|
61
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
62
62
|
[](https://www.npmjs.com/package/aidevops)
|
|
63
63
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
64
64
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.29.
|
|
1
|
+
3.29.9
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
|
-
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useState } from "react";
|
|
2
|
+
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { FiChevronDown, FiDownload, FiExternalLink, FiRefreshCw, FiRepeat, FiTrash2 } from "react-icons/fi";
|
|
4
4
|
import type { InventoryColumn } from "./app-model";
|
|
5
5
|
import { installationRows, text } from "./app-model";
|
|
@@ -12,17 +12,19 @@ interface DraftInventoryRow {
|
|
|
12
12
|
let draftRowCounter = 0;
|
|
13
13
|
|
|
14
14
|
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
15
|
+
const [appCollection, setAppCollection] = useState<AppCollectionId>("aidevops");
|
|
16
|
+
const [managedCategory, setManagedCategory] = useState<ManagedCategoryId>("core");
|
|
17
|
+
const [recommendedOs, setRecommendedOs] = useState<RecommendedOsId>("all");
|
|
15
18
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
16
|
-
const [
|
|
19
|
+
const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
|
|
17
20
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
18
21
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
22
|
+
const visibleManagedApps = sortedManagedApps(status.managed_apps).filter((app) => managedCategoryForApp(app) === managedCategory);
|
|
23
|
+
const visibleRecommendedApps = recommendedApps.filter((app) => recommendedOs === "all" || (app.os as readonly RecommendedOsId[]).includes(recommendedOs));
|
|
19
24
|
|
|
20
25
|
useEffect(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
setExpandedAppIds((current) => current.size === 0 ? new Set([status.managed_apps[0].id]) : current);
|
|
25
|
-
}, [status.managed_apps]);
|
|
26
|
+
setExpandedAppId(visibleManagedApps[0]?.id ?? null);
|
|
27
|
+
}, [managedCategory, status.managed_apps]);
|
|
26
28
|
|
|
27
29
|
useEffect(() => {
|
|
28
30
|
if (selectedJob === null || selectedJob.status !== "running") {
|
|
@@ -37,29 +39,33 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
37
39
|
}, [selectedJob]);
|
|
38
40
|
|
|
39
41
|
return (
|
|
40
|
-
<section className="
|
|
42
|
+
<section className="apps-surface" aria-label={text.apps}>
|
|
41
43
|
<div className="section-heading">
|
|
42
44
|
<p className="eyebrow">{text.infrastructure}</p>
|
|
43
45
|
<h2>{text.apps}</h2>
|
|
44
46
|
<p>{text.appsIntro}</p>
|
|
45
47
|
</div>
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
<TabNav label="Apps collections" tabs={appCollectionTabs} value={appCollection} onChange={(value) => setAppCollection(value)} />
|
|
49
|
+
{appCollection === "aidevops" ? <>
|
|
50
|
+
<TabNav label="aidevops app groups" tabs={managedCategoryTabs} value={managedCategory} onChange={(value) => setManagedCategory(value)} />
|
|
51
|
+
<div className="managed-app-list">
|
|
52
|
+
{visibleManagedApps.map((app) => (
|
|
53
|
+
<ManagedAppPanel
|
|
54
|
+
app={app}
|
|
55
|
+
expanded={expandedAppId === app.id}
|
|
56
|
+
job={jobForApp(app.id, jobs, selectedJob)}
|
|
57
|
+
key={app.id}
|
|
58
|
+
onJob={(job) => {
|
|
59
|
+
setJobs((current) => ({ ...current, [job.id]: job }));
|
|
60
|
+
setSelectedJobId(job.id);
|
|
61
|
+
setExpandedAppId(app.id);
|
|
62
|
+
}}
|
|
63
|
+
onToggle={() => setExpandedAppId((current) => current === app.id ? null : app.id)}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
<p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream inside each app panel. xterm.js with a node-pty bridge is the right next step for full TUI apps such as OpenCode; this view starts with non-interactive command logs.</p>
|
|
68
|
+
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} setOs={setRecommendedOs} />}
|
|
63
69
|
</section>
|
|
64
70
|
);
|
|
65
71
|
}
|
|
@@ -74,7 +80,6 @@ function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiMana
|
|
|
74
80
|
<span>{app.description}</span>
|
|
75
81
|
</span>
|
|
76
82
|
<span className="managed-app-summary-meta">
|
|
77
|
-
<SummaryChip label="Status" value={app.status} />
|
|
78
83
|
<SummaryChip label="Installed" value={app.installed_version} />
|
|
79
84
|
<SummaryChip label="Latest" value={app.latest_version} />
|
|
80
85
|
<FiChevronDown aria-hidden="true" className="managed-app-chevron" />
|
|
@@ -96,7 +101,7 @@ function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiMana
|
|
|
96
101
|
<AppMeta label="Installed" value={app.installed_version} />
|
|
97
102
|
<AppMeta label="Latest" value={app.latest_version} />
|
|
98
103
|
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
99
|
-
<
|
|
104
|
+
{job ? <AppLogLink job={job} /> : null}
|
|
100
105
|
</div>
|
|
101
106
|
{job ? <AppActionTerminal job={job} /> : <p className="empty-state compact-notice">No recent command output for this app. Run an action to open this app's terminal log.</p>}
|
|
102
107
|
</div> : null}
|
|
@@ -113,41 +118,73 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
|
|
|
113
118
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
return <a data-tooltip={
|
|
121
|
+
return <a data-tooltip={href} href={href} rel="noreferrer" target="_blank">{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
120
|
-
|
|
125
|
+
const [isChecked, setIsChecked] = useState(checked);
|
|
126
|
+
|
|
127
|
+
return <button aria-pressed={isChecked} className={isChecked ? "managed-toggle checked" : "managed-toggle"} onClick={() => setIsChecked((current) => !current)} type="button"><span aria-hidden="true" className={isChecked ? "switch-track checked" : "switch-track"}><span /></span>{label}</button>;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
|
|
124
131
|
return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
|
|
125
132
|
}
|
|
126
133
|
|
|
134
|
+
function AppLogLink({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
135
|
+
return <a className="app-meta app-log-link" href={`#app-job-${job.id}`}><small>Logs</small><strong>{job.action} · {job.status}</strong></a>;
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
function AppActionButton({ action, app, commandPreview, disabled, onJob }: { action: GuiAppActionId; app: GuiManagedAppSummary; commandPreview: string; disabled: boolean; onJob: (job: GuiAppActionJobSummary) => void }): ReactElement {
|
|
128
139
|
const icon = action === "install" ? <FiDownload /> : action === "update" ? <FiRefreshCw /> : action === "reinstall" ? <FiRepeat /> : <FiTrash2 />;
|
|
140
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
129
141
|
|
|
130
142
|
async function runAction(): Promise<void> {
|
|
131
143
|
if (disabled) {
|
|
132
144
|
return;
|
|
133
145
|
}
|
|
134
|
-
if ((action === "remove" || action === "reinstall") && !window.confirm(`${action} ${app.name}?`)) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
146
|
|
|
138
147
|
const response = await fetch(`/api/apps/${encodeURIComponent(app.id)}/actions/${action}`, { method: "POST" });
|
|
139
148
|
const envelope = await response.json() as GuiResponseEnvelope<GuiAppActionJobSummary>;
|
|
140
149
|
onJob(envelope.data);
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
return
|
|
152
|
+
return <>
|
|
153
|
+
<button aria-label={`${action} ${app.name}`} className={action === "remove" ? "app-action-button remove" : "app-action-button"} data-tooltip={commandPreview} disabled={disabled} onClick={() => setConfirmOpen(true)} type="button">{icon}<span>{action}</span></button>
|
|
154
|
+
{confirmOpen ? <ConfirmActionModal action={action} app={app} commandPreview={commandPreview} close={() => setConfirmOpen(false)} confirm={() => { setConfirmOpen(false); void runAction(); }} /> : null}
|
|
155
|
+
</>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ConfirmActionModal({ action, app, close, commandPreview, confirm }: { action: GuiAppActionId; app: GuiManagedAppSummary; close: () => void; commandPreview: string; confirm: () => void }): ReactElement {
|
|
159
|
+
return (
|
|
160
|
+
<div className="modal-backdrop" role="presentation">
|
|
161
|
+
<section aria-modal="true" className="confirm-modal" role="dialog" aria-labelledby={`confirm-${app.id}-${action}`}>
|
|
162
|
+
<p className="eyebrow">Confirm action</p>
|
|
163
|
+
<h3 id={`confirm-${app.id}-${action}`}>{action} {app.name}?</h3>
|
|
164
|
+
<p>This will start the allowlisted background command for this app.</p>
|
|
165
|
+
<code>{commandPreview}</code>
|
|
166
|
+
<div className="confirm-modal-actions">
|
|
167
|
+
<button className="secondary-action" onClick={close} type="button">Cancel</button>
|
|
168
|
+
<button className={action === "remove" ? "app-action-button remove" : "app-action-button"} onClick={confirm} type="button">Confirm</button>
|
|
169
|
+
</div>
|
|
170
|
+
</section>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
144
173
|
}
|
|
145
174
|
|
|
146
175
|
function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
176
|
+
const outputRef = useRef<HTMLPreElement | null>(null);
|
|
177
|
+
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (outputRef.current !== null) {
|
|
180
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
181
|
+
}
|
|
182
|
+
}, [job.output]);
|
|
183
|
+
|
|
147
184
|
return (
|
|
148
|
-
<section className="app-terminal" aria-label="App action terminal output">
|
|
185
|
+
<section className="app-terminal" id={`app-job-${job.id}`} aria-label="App action terminal output">
|
|
149
186
|
<header><strong>{job.app_id} {job.action}</strong><span>{job.status}{job.exit_code === null ? "" : ` (${job.exit_code})`}</span></header>
|
|
150
|
-
<pre>{job.output.join("\n")}</pre>
|
|
187
|
+
<pre ref={outputRef}>{job.output.join("\n")}</pre>
|
|
151
188
|
</section>
|
|
152
189
|
);
|
|
153
190
|
}
|
|
@@ -169,15 +206,151 @@ function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>,
|
|
|
169
206
|
return Object.values(jobs).reverse().find((job) => job.app_id === appId) ?? null;
|
|
170
207
|
}
|
|
171
208
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
209
|
+
function sortedManagedApps(apps: GuiManagedAppSummary[]): GuiManagedAppSummary[] {
|
|
210
|
+
return [...apps].sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function managedCategoryForApp(app: GuiManagedAppSummary): ManagedCategoryId {
|
|
214
|
+
if (["core", "safety", "automation"].includes(app.category)) {
|
|
215
|
+
return "core";
|
|
216
|
+
}
|
|
217
|
+
if (app.category.includes("ai")) {
|
|
218
|
+
return "ai";
|
|
219
|
+
}
|
|
220
|
+
if (["desktop", "editor"].includes(app.category)) {
|
|
221
|
+
return "desktop";
|
|
222
|
+
}
|
|
223
|
+
if (["terminal"].includes(app.category) || app.name.toLowerCase().includes("terminal")) {
|
|
224
|
+
return "terminal";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return "cli";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type AppCollectionId = "aidevops" | "recommended";
|
|
231
|
+
type ManagedCategoryId = "core" | "ai" | "cli" | "desktop" | "terminal";
|
|
232
|
+
type RecommendedOsId = "all" | "macos" | "linux" | "windows" | "ios" | "android";
|
|
233
|
+
|
|
234
|
+
interface TabOption<T extends string> {
|
|
235
|
+
id: T;
|
|
236
|
+
label: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface RecommendedApp {
|
|
240
|
+
name: string;
|
|
241
|
+
description: string;
|
|
242
|
+
websiteUrl: string;
|
|
243
|
+
alternativeToUrl?: string;
|
|
244
|
+
repoUrl?: string;
|
|
245
|
+
os: RecommendedOsId[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const appCollectionTabs: TabOption<AppCollectionId>[] = [
|
|
249
|
+
{ id: "aidevops", label: "aidevops" },
|
|
250
|
+
{ id: "recommended", label: "Recommended" },
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const managedCategoryTabs: TabOption<ManagedCategoryId>[] = [
|
|
254
|
+
{ id: "core", label: "Core" },
|
|
255
|
+
{ id: "ai", label: "AI" },
|
|
256
|
+
{ id: "cli", label: "CLI" },
|
|
257
|
+
{ id: "desktop", label: "Desktop" },
|
|
258
|
+
{ id: "terminal", label: "Terminal" },
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const recommendedOsTabs: TabOption<RecommendedOsId>[] = [
|
|
262
|
+
{ id: "all", label: "All" },
|
|
263
|
+
{ id: "macos", label: "macOS" },
|
|
264
|
+
{ id: "linux", label: "Linux" },
|
|
265
|
+
{ id: "windows", label: "Windows" },
|
|
266
|
+
{ id: "ios", label: "iOS" },
|
|
267
|
+
{ id: "android", label: "Android" },
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const recommendedApps = ([
|
|
271
|
+
{ name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"] },
|
|
272
|
+
{ name: "Bitwarden", description: "Open source password manager.", websiteUrl: "https://bitwarden.com", alternativeToUrl: "https://alternativeto.net/software/bitwarden--free-password-manager/", repoUrl: "https://github.com/bitwarden", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
273
|
+
{ name: "Brave Browser", description: "Privacy-focused web browser.", websiteUrl: "https://brave.com/", alternativeToUrl: "https://alternativeto.net/software/brave/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
274
|
+
{ name: "Cloudron", description: "Self-hosted app platform.", websiteUrl: "https://www.cloudron.io/", alternativeToUrl: "https://alternativeto.net/software/cloudron/", os: ["linux"] },
|
|
275
|
+
{ name: "Collabora Online", description: "Online office collaboration.", websiteUrl: "https://www.collaboraonline.com/collabora-online/", alternativeToUrl: "https://alternativeto.net/software/collabora-online/", os: ["linux"] },
|
|
276
|
+
{ name: "Cometly", description: "Marketing attribution analytics.", websiteUrl: "https://www.cometly.com/", os: [] },
|
|
277
|
+
{ name: "DaVinci Resolve", description: "Professional video editing, color, VFX, and audio post-production.", websiteUrl: "https://www.blackmagicdesign.com/products/davinciresolve/", alternativeToUrl: "https://alternativeto.net/software/davinci-resolve/", os: ["macos", "linux", "windows"] },
|
|
278
|
+
{ name: "DocuSeal", description: "Open source document signing.", websiteUrl: "https://www.docuseal.com/", alternativeToUrl: "https://alternativeto.net/software/docuseal/", os: ["linux"] },
|
|
279
|
+
{ name: "Element", description: "Matrix messaging client.", websiteUrl: "https://element.io/", alternativeToUrl: "https://alternativeto.net/software/element-app/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
280
|
+
{ name: "Enpass", description: "Offline-first password manager.", websiteUrl: "https://www.enpass.io/", alternativeToUrl: "https://alternativeto.net/software/enpass/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
281
|
+
{ name: "EspoCRM", description: "Open source CRM.", websiteUrl: "https://www.espocrm.com/", alternativeToUrl: "https://alternativeto.net/software/espocrm/", os: ["linux"] },
|
|
282
|
+
{ name: "Fathom Analytics", description: "Privacy-first analytics.", websiteUrl: "https://usefathom.com/", alternativeToUrl: "https://alternativeto.net/software/fathom-analytics/", os: [] },
|
|
283
|
+
{ name: "FonrBase", description: "Font management and reference tool.", websiteUrl: "https://fontba.se/", alternativeToUrl: "https://alternativeto.net/software/fontbase/", os: ["macos"] },
|
|
284
|
+
{ name: "Forgejo", description: "Self-hosted Git forge.", websiteUrl: "https://forgejo.org/", alternativeToUrl: "https://alternativeto.net/software/forgejo/", os: ["linux"] },
|
|
285
|
+
{ name: "Ghost", description: "Publishing platform.", websiteUrl: "https://ghost.org/", alternativeToUrl: "https://alternativeto.net/software/ghost/", os: ["linux"] },
|
|
286
|
+
{ name: "Gitea", description: "Self-hosted Git service.", websiteUrl: "https://about.gitea.com/", alternativeToUrl: "https://alternativeto.net/software/gitea/", os: ["linux", "windows"] },
|
|
287
|
+
{ name: "GitLab", description: "DevSecOps and Git hosting platform.", websiteUrl: "https://gitlab.com/", alternativeToUrl: "https://alternativeto.net/software/gitlab/", os: ["linux"] },
|
|
288
|
+
{ name: "LibreOffice", description: "Open source office suite.", websiteUrl: "https://www.libreoffice.org/", alternativeToUrl: "https://alternativeto.net/software/libreoffice/", os: ["macos", "linux", "windows"] },
|
|
289
|
+
{ name: "LocalWP", description: "Local WordPress development.", websiteUrl: "https://localwp.com/", alternativeToUrl: "https://alternativeto.net/software/local-by-flywheel/", os: ["macos", "linux", "windows"] },
|
|
290
|
+
{ name: "Matomo", description: "Open analytics platform.", websiteUrl: "https://matomo.org/", alternativeToUrl: "https://alternativeto.net/software/piwik/", os: ["linux"] },
|
|
291
|
+
{ name: "Nextcloud", description: "Open source content collaboration platform.", websiteUrl: "https://nextcloud.com/", alternativeToUrl: "https://alternativeto.net/software/nextcloud/", repoUrl: "https://github.com/nextcloud", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
292
|
+
{ name: "OBS Studio", description: "Open source video recording and live streaming.", websiteUrl: "https://obsproject.com/", alternativeToUrl: "https://alternativeto.net/software/open-broadcaster-software/", repoUrl: "https://github.com/obsproject/obs-studio", os: ["macos", "linux", "windows"] },
|
|
293
|
+
{ name: "ONLYOFFICE", description: "Office and document collaboration.", websiteUrl: "https://www.onlyoffice.com/", alternativeToUrl: "https://alternativeto.net/software/onlyoffice/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
294
|
+
{ name: "OpenScreen", description: "Open screen-sharing project.", websiteUrl: "https://github.com/getopenscreen/openscreen", alternativeToUrl: "https://alternativeto.net/software/openscreen/", repoUrl: "https://github.com/getopenscreen/openscreen", os: ["linux"] },
|
|
295
|
+
{ name: "Osaurus", description: "AI app workspace.", websiteUrl: "https://osaurus.ai/", alternativeToUrl: "https://alternativeto.net/software/osaurus/", os: ["macos"] },
|
|
296
|
+
{ name: "Parallels Desktop", description: "Run Windows, Linux, and virtual machines on macOS.", websiteUrl: "https://www.parallels.com/products/desktop/", alternativeToUrl: "https://alternativeto.net/software/parallels-desktop/about/", os: ["macos"] },
|
|
297
|
+
{ name: "PDF Studio", description: "PDF editor.", websiteUrl: "https://www.qoppa.com/pdfstudio/", alternativeToUrl: "https://alternativeto.net/software/qoppa-pdf-studio/", os: ["macos", "linux", "windows"] },
|
|
298
|
+
{ name: "PostHog", description: "Product analytics platform.", websiteUrl: "https://posthog.com/", alternativeToUrl: "https://alternativeto.net/software/posthog/", os: ["linux"] },
|
|
299
|
+
{ name: "Postiz", description: "Social media scheduling.", websiteUrl: "https://postiz.com/", alternativeToUrl: "https://alternativeto.net/software/postiz/", os: ["linux"] },
|
|
300
|
+
{ name: "PrivateBin", description: "Zero-knowledge pastebin.", websiteUrl: "https://privatebin.info/", alternativeToUrl: "https://alternativeto.net/software/privatebin/", repoUrl: "https://github.com/PrivateBin/PrivateBin", os: ["linux"] },
|
|
301
|
+
{ name: "Proxmox", description: "Virtualization platform.", websiteUrl: "https://www.proxmox.com/", alternativeToUrl: "https://alternativeto.net/software/proxmox-virtual-environment/", os: ["linux"] },
|
|
302
|
+
{ name: "QuickFile", description: "Accounting platform.", websiteUrl: "https://www.quickfile.co.uk/", os: [] },
|
|
303
|
+
{ name: "Reframed", description: "Creative framing utility.", websiteUrl: "https://www.reframed.dev/", alternativeToUrl: "https://alternativeto.net/software/reframed/", os: ["macos"] },
|
|
304
|
+
{ name: "Rybbit", description: "Web analytics.", websiteUrl: "https://rybbit.com/", alternativeToUrl: "https://alternativeto.net/software/rybbit/", os: ["linux"] },
|
|
305
|
+
{ name: "SchildiChat", description: "Matrix messaging client.", websiteUrl: "https://schildi.chat/", alternativeToUrl: "https://alternativeto.net/software/schildichat/", os: ["macos", "linux", "windows", "android"] },
|
|
306
|
+
{ name: "ScreenFlow", description: "Screen recording and editing.", websiteUrl: "https://www.telestream.net/screenflow/", alternativeToUrl: "https://alternativeto.net/software/screenflow/", os: ["macos", "ios"] },
|
|
307
|
+
{ name: "SEO Utils", description: "Desktop SEO tools.", websiteUrl: "https://seoutils.app/", os: ["macos", "linux", "windows"] },
|
|
308
|
+
{ name: "Shottr", description: "macOS screenshot utility.", websiteUrl: "https://shottr.cc/", alternativeToUrl: "https://alternativeto.net/software/shottr/", os: ["macos"] },
|
|
309
|
+
{ name: "Signal", description: "Private messenger.", websiteUrl: "https://signal.org/", alternativeToUrl: "https://alternativeto.net/software/signal-private-messenger/", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
310
|
+
{ name: "SimpleX Chat", description: "Private messenger with no user IDs.", websiteUrl: "https://simplex.chat/", alternativeToUrl: "https://alternativeto.net/software/simplex-chat/about/", repoUrl: "https://github.com/simplex-chat", os: ["macos", "linux", "windows", "ios", "android"] },
|
|
311
|
+
{ name: "Thunderbird", description: "Email and calendar app.", websiteUrl: "https://www.thunderbird.net/", alternativeToUrl: "https://alternativeto.net/software/mozilla-thunderbird/about/", os: ["macos", "linux", "windows"] },
|
|
312
|
+
{ name: "Ubicloud", description: "Open cloud platform.", websiteUrl: "https://www.ubicloud.com/", alternativeToUrl: "https://alternativeto.net/software/ubicloud/about/", os: ["linux"] },
|
|
313
|
+
{ name: "Vaultwarden", description: "Alternative Bitwarden server implementation.", websiteUrl: "https://github.com/dani-garcia/vaultwarden", alternativeToUrl: "https://alternativeto.net/software/vaultwarden/", repoUrl: "https://github.com/dani-garcia/vaultwarden", os: ["linux"] },
|
|
314
|
+
{ name: "VideoProc", description: "Video processing toolkit.", websiteUrl: "https://www.videoproc.com/", alternativeToUrl: "https://alternativeto.net/software/videoproc/about/", os: ["macos", "windows"] },
|
|
315
|
+
{ name: "VirtualBox", description: "Virtualization app.", websiteUrl: "https://www.virtualbox.org/", alternativeToUrl: "https://alternativeto.net/software/virtualbox/about/", os: ["macos", "linux", "windows"] },
|
|
316
|
+
{ name: "WordPress", description: "Open source publishing platform.", websiteUrl: "https://wordpress.org/", alternativeToUrl: "https://alternativeto.net/software/wordpress/about/", os: ["linux"] },
|
|
317
|
+
] satisfies RecommendedApp[]).sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
318
|
+
|
|
319
|
+
function TabNav<T extends string>({ label, onChange, tabs, value }: { label: string; onChange: (value: T) => void; tabs: TabOption<T>[]; value: T }): ReactElement {
|
|
320
|
+
return <div aria-label={label} className="pill-tabs app-subnav" role="tablist">{tabs.map((tab) => <button aria-selected={value === tab.id} className={value === tab.id ? "active" : ""} key={tab.id} onClick={() => onChange(tab.id)} role="tab" type="button">{tab.label}</button>)}</div>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function RecommendedAppsSurface({ apps, os, setOs }: { apps: RecommendedApp[]; os: RecommendedOsId; setOs: (os: RecommendedOsId) => void }): ReactElement {
|
|
324
|
+
return <>
|
|
325
|
+
<TabNav label="Recommended app OS filters" tabs={recommendedOsTabs} value={os} onChange={setOs} />
|
|
326
|
+
<div className="recommended-app-grid">
|
|
327
|
+
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} />)}
|
|
328
|
+
</div>
|
|
329
|
+
</>;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function RecommendedAppCard({ app }: { app: RecommendedApp }): ReactElement {
|
|
333
|
+
return <article className="recommended-app-card">
|
|
334
|
+
<div>
|
|
335
|
+
<strong>{app.name}</strong>
|
|
336
|
+
<p>{app.description}</p>
|
|
337
|
+
</div>
|
|
338
|
+
<OsIconList os={app.os} />
|
|
339
|
+
<div className="managed-app-links plain-links">
|
|
340
|
+
<OriginLink href={app.websiteUrl} label={text.website} />
|
|
341
|
+
{app.alternativeToUrl ? <OriginLink href={app.alternativeToUrl} label="AlternativeTo" /> : null}
|
|
342
|
+
{app.repoUrl ? <OriginLink href={app.repoUrl} label="Repo" /> : null}
|
|
343
|
+
</div>
|
|
344
|
+
</article>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function OsIconList({ os }: { os: RecommendedOsId[] }): ReactElement {
|
|
348
|
+
const icons: Record<RecommendedOsId, string> = { all: "*", macos: "⌘", linux: "🐧", windows: "⊞", ios: "iOS", android: "🤖" };
|
|
349
|
+
if (os.length === 0) {
|
|
350
|
+
return <span className="os-icon-list"><span title="Web app">Web</span></span>;
|
|
178
351
|
}
|
|
179
352
|
|
|
180
|
-
return
|
|
353
|
+
return <span className="os-icon-list">{os.map((item) => <span key={item} title={recommendedOsTabs.find((tab) => tab.id === item)?.label}>{icons[item]}</span>)}</span>;
|
|
181
354
|
}
|
|
182
355
|
|
|
183
356
|
export function InstallationSurface(): ReactElement {
|
|
@@ -1964,6 +1964,11 @@ code {
|
|
|
1964
1964
|
margin-bottom: 12px;
|
|
1965
1965
|
}
|
|
1966
1966
|
|
|
1967
|
+
.apps-surface {
|
|
1968
|
+
display: grid;
|
|
1969
|
+
gap: 14px;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1967
1972
|
.data-table,
|
|
1968
1973
|
.editable-table,
|
|
1969
1974
|
.installation-list,
|
|
@@ -2039,13 +2044,16 @@ code {
|
|
|
2039
2044
|
}
|
|
2040
2045
|
|
|
2041
2046
|
.managed-summary-chip {
|
|
2047
|
+
align-items: center;
|
|
2042
2048
|
background: color-mix(in srgb, var(--surface-elevated) 92%, transparent);
|
|
2043
2049
|
border: 1px solid var(--border);
|
|
2044
2050
|
border-radius: 999px;
|
|
2045
2051
|
display: inline-grid;
|
|
2046
2052
|
gap: 1px;
|
|
2053
|
+
justify-items: center;
|
|
2047
2054
|
min-width: 118px;
|
|
2048
2055
|
padding: 7px 11px;
|
|
2056
|
+
text-align: center;
|
|
2049
2057
|
}
|
|
2050
2058
|
|
|
2051
2059
|
.managed-summary-chip small {
|
|
@@ -2106,25 +2114,22 @@ code {
|
|
|
2106
2114
|
.managed-app-links a,
|
|
2107
2115
|
.origin-missing {
|
|
2108
2116
|
align-items: center;
|
|
2109
|
-
border: 1px solid var(--border);
|
|
2110
|
-
border-radius: 999px;
|
|
2111
2117
|
color: var(--text-secondary);
|
|
2112
2118
|
display: inline-flex;
|
|
2113
2119
|
font-size: 0.78rem;
|
|
2114
2120
|
font-weight: 800;
|
|
2115
2121
|
gap: 4px;
|
|
2116
2122
|
max-width: 100%;
|
|
2117
|
-
padding:
|
|
2123
|
+
padding: 4px 0;
|
|
2118
2124
|
position: relative;
|
|
2119
2125
|
text-decoration: none;
|
|
2120
2126
|
}
|
|
2121
2127
|
|
|
2122
2128
|
.managed-app-links a:hover,
|
|
2123
2129
|
.managed-app-links a:focus-visible {
|
|
2124
|
-
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
2125
|
-
border-color: color-mix(in srgb, var(--accent) 48%, var(--border));
|
|
2126
2130
|
color: var(--accent);
|
|
2127
2131
|
outline: none;
|
|
2132
|
+
text-decoration: underline;
|
|
2128
2133
|
}
|
|
2129
2134
|
|
|
2130
2135
|
.origin-missing {
|
|
@@ -2157,9 +2162,20 @@ code {
|
|
|
2157
2162
|
text-transform: uppercase;
|
|
2158
2163
|
}
|
|
2159
2164
|
|
|
2165
|
+
button.managed-toggle {
|
|
2166
|
+
cursor: pointer;
|
|
2167
|
+
text-align: left;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
button.managed-toggle:hover,
|
|
2171
|
+
button.managed-toggle:focus-visible {
|
|
2172
|
+
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
|
2173
|
+
outline: none;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2160
2176
|
.switch-track {
|
|
2161
|
-
background: var(--surface-muted);
|
|
2162
|
-
border: 1px solid var(--border);
|
|
2177
|
+
background: color-mix(in srgb, #ef4444 18%, var(--surface-muted));
|
|
2178
|
+
border: 1px solid color-mix(in srgb, #ef4444 44%, var(--border));
|
|
2163
2179
|
border-radius: 999px;
|
|
2164
2180
|
display: inline-flex;
|
|
2165
2181
|
padding: 2px;
|
|
@@ -2167,7 +2183,7 @@ code {
|
|
|
2167
2183
|
}
|
|
2168
2184
|
|
|
2169
2185
|
.switch-track span {
|
|
2170
|
-
background:
|
|
2186
|
+
background: #ef4444;
|
|
2171
2187
|
border-radius: 50%;
|
|
2172
2188
|
height: 14px;
|
|
2173
2189
|
transform: translateX(0);
|
|
@@ -2176,10 +2192,15 @@ code {
|
|
|
2176
2192
|
}
|
|
2177
2193
|
|
|
2178
2194
|
.switch-track.checked span {
|
|
2179
|
-
background:
|
|
2195
|
+
background: #22c55e;
|
|
2180
2196
|
transform: translateX(14px);
|
|
2181
2197
|
}
|
|
2182
2198
|
|
|
2199
|
+
.switch-track.checked {
|
|
2200
|
+
background: color-mix(in srgb, #22c55e 18%, var(--surface-muted));
|
|
2201
|
+
border-color: color-mix(in srgb, #22c55e 48%, var(--border));
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2183
2204
|
.app-meta small {
|
|
2184
2205
|
color: var(--text-muted);
|
|
2185
2206
|
font-size: 0.72rem;
|
|
@@ -2195,6 +2216,17 @@ code {
|
|
|
2195
2216
|
word-break: break-word;
|
|
2196
2217
|
}
|
|
2197
2218
|
|
|
2219
|
+
.app-log-link {
|
|
2220
|
+
color: var(--text-primary);
|
|
2221
|
+
text-decoration: none;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
.app-log-link:hover,
|
|
2225
|
+
.app-log-link:focus-visible {
|
|
2226
|
+
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
|
2227
|
+
outline: none;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2198
2230
|
.app-action-button {
|
|
2199
2231
|
align-items: center;
|
|
2200
2232
|
background: color-mix(in srgb, var(--accent) 16%, var(--surface-elevated));
|
|
@@ -2278,6 +2310,45 @@ code {
|
|
|
2278
2310
|
word-break: break-word;
|
|
2279
2311
|
}
|
|
2280
2312
|
|
|
2313
|
+
.modal-backdrop {
|
|
2314
|
+
align-items: center;
|
|
2315
|
+
background: rgb(0 0 0 / 62%);
|
|
2316
|
+
display: flex;
|
|
2317
|
+
inset: 0;
|
|
2318
|
+
justify-content: center;
|
|
2319
|
+
padding: 18px;
|
|
2320
|
+
position: fixed;
|
|
2321
|
+
z-index: 100;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
.confirm-modal {
|
|
2325
|
+
background: var(--surface-elevated);
|
|
2326
|
+
border: 1px solid var(--border-hover);
|
|
2327
|
+
border-radius: 18px;
|
|
2328
|
+
box-shadow: 0 24px 90px rgb(0 0 0 / 48%);
|
|
2329
|
+
display: grid;
|
|
2330
|
+
gap: 12px;
|
|
2331
|
+
max-width: 520px;
|
|
2332
|
+
padding: 22px;
|
|
2333
|
+
width: min(100%, 520px);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
.confirm-modal h3,
|
|
2337
|
+
.confirm-modal p {
|
|
2338
|
+
margin: 0;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
.confirm-modal code {
|
|
2342
|
+
overflow-wrap: anywhere;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.confirm-modal-actions {
|
|
2346
|
+
display: flex;
|
|
2347
|
+
flex-wrap: wrap;
|
|
2348
|
+
gap: 10px;
|
|
2349
|
+
justify-content: flex-end;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2281
2352
|
[data-tooltip] {
|
|
2282
2353
|
position: relative;
|
|
2283
2354
|
}
|
|
@@ -2388,6 +2459,57 @@ code {
|
|
|
2388
2459
|
width: min(100%, 520px);
|
|
2389
2460
|
}
|
|
2390
2461
|
|
|
2462
|
+
.app-subnav {
|
|
2463
|
+
width: fit-content;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.recommended-app-grid {
|
|
2467
|
+
display: grid;
|
|
2468
|
+
gap: 12px;
|
|
2469
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
.recommended-app-card {
|
|
2473
|
+
background:
|
|
2474
|
+
linear-gradient(135deg, color-mix(in srgb, var(--accent) 6%, transparent), transparent 40%),
|
|
2475
|
+
var(--surface-muted);
|
|
2476
|
+
border: 1px solid var(--border);
|
|
2477
|
+
border-radius: 18px;
|
|
2478
|
+
display: grid;
|
|
2479
|
+
gap: 12px;
|
|
2480
|
+
padding: 16px;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
.recommended-app-card strong {
|
|
2484
|
+
font-size: 1.05rem;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
.recommended-app-card p {
|
|
2488
|
+
color: var(--text-secondary);
|
|
2489
|
+
margin: 4px 0 0;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
.os-icon-list {
|
|
2493
|
+
display: flex;
|
|
2494
|
+
flex-wrap: wrap;
|
|
2495
|
+
gap: 6px;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
.os-icon-list span {
|
|
2499
|
+
align-items: center;
|
|
2500
|
+
background: var(--surface-elevated);
|
|
2501
|
+
border: 1px solid var(--border);
|
|
2502
|
+
border-radius: 999px;
|
|
2503
|
+
color: var(--text-secondary);
|
|
2504
|
+
display: inline-flex;
|
|
2505
|
+
font-size: 0.72rem;
|
|
2506
|
+
font-weight: 900;
|
|
2507
|
+
justify-content: center;
|
|
2508
|
+
min-height: 28px;
|
|
2509
|
+
min-width: 28px;
|
|
2510
|
+
padding: 4px 7px;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2391
2513
|
.pill-tabs button {
|
|
2392
2514
|
background: transparent;
|
|
2393
2515
|
border: 0;
|
package/setup.sh
CHANGED
|
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
|
|
|
12
12
|
# AI Assistant Server Access Framework Setup Script
|
|
13
13
|
# Helps developers set up the framework for their infrastructure
|
|
14
14
|
#
|
|
15
|
-
# Version: 3.29.
|
|
15
|
+
# Version: 3.29.9
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|