aidevops 3.29.9 → 3.29.11
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-api/src/app.ts +1 -0
- package/packages/gui-api/src/status-managed-apps.ts +23 -8
- package/packages/gui-web/src/InventorySurfaces.tsx +224 -77
- package/packages/gui-web/src/app-model.ts +13 -13
- package/packages/gui-web/src/styles.css +82 -20
- 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.11
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ const appActionCommands: Record<string, Partial<Record<GuiAppActionId, string[]>
|
|
|
10
10
|
"gui-desktop": { install: ["aidevops", "setup", "--scope", "gui-desktop"], update: ["aidevops", "setup", "--scope", "gui-desktop"], reinstall: ["aidevops", "setup", "--scope", "gui-desktop"] },
|
|
11
11
|
hooks: { install: ["aidevops", "setup", "--scope", "hooks"], update: ["aidevops", "setup", "--scope", "hooks"], reinstall: ["aidevops", "setup", "--scope", "hooks"] },
|
|
12
12
|
opencode: { install: ["aidevops", "setup", "--scope", "opencode"], update: ["aidevops", "setup", "--scope", "opencode"], reinstall: ["aidevops", "setup", "--scope", "opencode"] },
|
|
13
|
+
"opencode-cli": { install: ["aidevops", "setup", "--scope", "opencode"], update: ["aidevops", "setup", "--scope", "opencode"], reinstall: ["aidevops", "setup", "--scope", "opencode"] },
|
|
13
14
|
pulse: { install: ["aidevops", "setup", "--scope", "pulse"], update: ["aidevops", "setup", "--scope", "pulse"], reinstall: ["aidevops", "setup", "--scope", "pulse"] },
|
|
14
15
|
tabby: { install: ["aidevops", "setup", "--scope", "tabby"], update: ["aidevops", "setup", "--scope", "tabby"], reinstall: ["aidevops", "setup", "--scope", "tabby"] },
|
|
15
16
|
bun: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
|
|
@@ -83,12 +83,12 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
|
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
id: "opencode",
|
|
86
|
-
name: "OpenCode",
|
|
87
|
-
description: "
|
|
88
|
-
category: "
|
|
89
|
-
binary:
|
|
90
|
-
version_args: [
|
|
91
|
-
install_path_refs: ["~/Applications/OpenCode AIDevOps.app", "/Applications/OpenCode.app", "~/.config/opencode/opencode.json"],
|
|
86
|
+
name: "OpenCode Beta",
|
|
87
|
+
description: "Native OpenCode beta desktop app configured by aidevops.",
|
|
88
|
+
category: "desktop",
|
|
89
|
+
binary: null,
|
|
90
|
+
version_args: [],
|
|
91
|
+
install_path_refs: ["~/Applications/OpenCode AIDevOps.app", "~/Applications/OpenCode.app", "/Applications/OpenCode.app", "~/.config/opencode/opencode.json"],
|
|
92
92
|
origin_website_url: "https://opencode.ai",
|
|
93
93
|
origin_repo_url: "",
|
|
94
94
|
aidevops_install: true,
|
|
@@ -96,6 +96,7 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
|
|
|
96
96
|
latest_version_source: "binary",
|
|
97
97
|
action_commands: setupScopeActions("opencode"),
|
|
98
98
|
},
|
|
99
|
+
binaryApp({ id: "opencode-cli", name: "OpenCode CLI", description: "Command-line runtime installed with OpenCode and configured by aidevops.", category: "cli", binary: "opencode", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/opencode", "/usr/local/bin/opencode", "~/.bun/bin/opencode", "~/.config/opencode/opencode.json"], origin_website_url: "https://opencode.ai", origin_repo_url: "", aidevops_install: true, aidevops_update: true }),
|
|
99
100
|
{
|
|
100
101
|
id: "hooks",
|
|
101
102
|
name: "Safety hooks",
|
|
@@ -139,7 +140,7 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
|
|
|
139
140
|
binaryApp({ id: "homebrew", name: "Homebrew", description: "macOS package manager used by setup/update when available.", category: "package manager", binary: "brew", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"], origin_website_url: "https://brew.sh", origin_repo_url: "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh" }),
|
|
140
141
|
binaryApp({ id: "qlty", name: "Qlty CLI", description: "Optional code quality aggregator installed by setup when selected.", category: "quality", binary: "qlty", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/qlty", "/usr/local/bin/qlty", "~/.qlty/bin/qlty"], origin_website_url: "https://qlty.sh", origin_repo_url: "" }),
|
|
141
142
|
binaryApp({ id: "rtk", name: "rtk", description: "Token-optimized GitHub/API helper used by interactive discovery.", category: "developer tooling", binary: "rtk", version_args: ["--version"], install_path_refs: ["~/.local/bin/rtk", "/opt/homebrew/bin/rtk", "/usr/local/bin/rtk"], origin_website_url: "https://github.com/rtk-ai/rtk#installation", origin_repo_url: "https://github.com/rtk-ai/rtk" }),
|
|
142
|
-
binaryApp({ id: "cursor", name: "Cursor CLI", description: "Cursor command-line integration and config targets.", category: "
|
|
143
|
+
binaryApp({ id: "cursor", name: "Cursor CLI", description: "Cursor command-line integration and config targets.", category: "cli", binary: "cursor", version_args: ["--version"], install_path_refs: ["~/.cursor/bin/cursor", "/Applications/Cursor.app"], origin_website_url: "https://cursor.com/install", origin_repo_url: "" }),
|
|
143
144
|
binaryApp({ id: "zed", name: "Zed", description: "Optional editor installed by setup when selected.", category: "editor", binary: "zed", version_args: ["--version"], install_path_refs: ["/Applications/Zed.app", "/opt/homebrew/bin/zed", "/usr/local/bin/zed"], origin_website_url: "https://zed.dev/download", origin_repo_url: "", aidevops_install: false, aidevops_update: false }),
|
|
144
145
|
binaryApp({ id: "orbstack", name: "OrbStack", description: "Optional local container/VM runtime for macOS development.", category: "runtime", binary: "orb", version_args: ["version"], install_path_refs: ["/Applications/OrbStack.app", "/opt/homebrew/bin/orb"], origin_website_url: "https://orbstack.dev/", origin_repo_url: "", aidevops_install: false, aidevops_update: false }),
|
|
145
146
|
binaryApp({ id: "ollama", name: "Ollama", description: "Optional local model runtime for local AI workflows.", category: "ai runtime", binary: "ollama", version_args: ["--version"], install_path_refs: ["/Applications/Ollama.app", "/opt/homebrew/bin/ollama", "/usr/local/bin/ollama"], origin_website_url: "https://ollama.com", origin_repo_url: "" }),
|
|
@@ -221,8 +222,22 @@ function readManagedAppVersion(definition: ManagedAppDefinition, binaryPath: str
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
if (binaryPath === null) {
|
|
224
|
-
|
|
225
|
+
const existingPathRef = firstExistingPathRef(definition.install_path_refs);
|
|
226
|
+
|
|
227
|
+
if (existingPathRef === null) {
|
|
228
|
+
return "not installed";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return readAppBundleVersion(existingPathRef) ?? "installed";
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
return readBinaryVersion(binaryPath, definition.version_args);
|
|
228
235
|
}
|
|
236
|
+
|
|
237
|
+
function readAppBundleVersion(pathRef: string): string | null {
|
|
238
|
+
if (!pathRef.endsWith(".app")) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return readBinaryVersion("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleShortVersionString", `${expandHome(pathRef)}/Contents/Info.plist`]);
|
|
243
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
|
-
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { type Dispatch, type MouseEvent as ReactMouseEvent, type ReactElement, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
3
|
+
import type { IconType } from "react-icons";
|
|
4
|
+
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
|
|
5
|
+
import { FiChevronDown, FiCode, FiDownload, FiExternalLink, FiGlobe, FiMonitor, FiRefreshCw, FiRepeat, FiTerminal, FiTrash2 } from "react-icons/fi";
|
|
6
|
+
import { IoLogoAndroid } from "react-icons/io";
|
|
7
|
+
import { SiIos } from "react-icons/si";
|
|
4
8
|
import type { InventoryColumn } from "./app-model";
|
|
5
9
|
import { installationRows, text } from "./app-model";
|
|
6
10
|
|
|
@@ -14,13 +18,16 @@ let draftRowCounter = 0;
|
|
|
14
18
|
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
15
19
|
const [appCollection, setAppCollection] = useState<AppCollectionId>("aidevops");
|
|
16
20
|
const [managedCategory, setManagedCategory] = useState<ManagedCategoryId>("core");
|
|
17
|
-
const [
|
|
21
|
+
const [recommendedPlatform, setRecommendedPlatform] = useState<RecommendedPlatformFilterId>("all");
|
|
18
22
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
19
23
|
const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
|
|
20
24
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
25
|
+
const [policyJobs, setPolicyJobs] = useState<Record<string, GuiAppActionJobSummary[]>>({});
|
|
26
|
+
const [policyToggles, setPolicyToggles] = useState<ManagedPolicyToggleState>(() => readManagedPolicyToggles());
|
|
21
27
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
22
|
-
const
|
|
23
|
-
const
|
|
28
|
+
const managedApps = sortedManagedApps(status.managed_apps).map((app) => applyManagedPolicyToggles(app, policyToggles[app.id]));
|
|
29
|
+
const visibleManagedApps = managedApps.filter((app) => managedCategoryForApp(app) === managedCategory);
|
|
30
|
+
const visibleRecommendedApps = recommendedApps.filter((app) => recommendedPlatform === "all" || app.platforms.includes(recommendedPlatform));
|
|
24
31
|
|
|
25
32
|
useEffect(() => {
|
|
26
33
|
setExpandedAppId(visibleManagedApps[0]?.id ?? null);
|
|
@@ -55,22 +62,36 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
55
62
|
expanded={expandedAppId === app.id}
|
|
56
63
|
job={jobForApp(app.id, jobs, selectedJob)}
|
|
57
64
|
key={app.id}
|
|
65
|
+
policyJobs={policyJobs[app.id] ?? []}
|
|
58
66
|
onJob={(job) => {
|
|
59
67
|
setJobs((current) => ({ ...current, [job.id]: job }));
|
|
60
68
|
setSelectedJobId(job.id);
|
|
61
69
|
setExpandedAppId(app.id);
|
|
62
70
|
}}
|
|
71
|
+
onPolicyToggle={(policy, value) => {
|
|
72
|
+
setPolicyToggles((current) => {
|
|
73
|
+
const next = { ...current, [app.id]: { ...current[app.id], [policy]: value } };
|
|
74
|
+
writeManagedPolicyToggles(next);
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
const policyJob = createPolicyJob(app.id, policy, value);
|
|
78
|
+
setPolicyJobs((current) => ({ ...current, [app.id]: [...(current[app.id] ?? []), policyJob] }));
|
|
79
|
+
setSelectedJobId(policyJob.id);
|
|
80
|
+
setExpandedAppId(app.id);
|
|
81
|
+
}}
|
|
63
82
|
onToggle={() => setExpandedAppId((current) => current === app.id ? null : app.id)}
|
|
64
83
|
/>
|
|
65
84
|
))}
|
|
66
85
|
</div>
|
|
67
86
|
<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}
|
|
87
|
+
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} platform={recommendedPlatform} setPlatform={setRecommendedPlatform} />}
|
|
69
88
|
</section>
|
|
70
89
|
);
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiManagedAppSummary; expanded: boolean; job: GuiAppActionJobSummary | null; onJob: (job: GuiAppActionJobSummary) => void; onToggle: () => void }): ReactElement {
|
|
92
|
+
function ManagedAppPanel({ app, expanded, job, onJob, onPolicyToggle, onToggle, policyJobs }: { app: GuiManagedAppSummary; expanded: boolean; job: GuiAppActionJobSummary | null; onJob: (job: GuiAppActionJobSummary) => void; onPolicyToggle: (policy: ManagedPolicyId, value: boolean) => void; onToggle: () => void; policyJobs: GuiAppActionJobSummary[] }): ReactElement {
|
|
93
|
+
const lockedPolicy = isEssentialManagedApp(app);
|
|
94
|
+
|
|
74
95
|
return (
|
|
75
96
|
<article className={expanded ? "managed-app-card expanded" : "managed-app-card"}>
|
|
76
97
|
<button aria-expanded={expanded} className="managed-app-summary" data-tooltip={`${expanded ? "Collapse" : "Expand"} ${app.name} controls`} onClick={onToggle} type="button">
|
|
@@ -94,16 +115,18 @@ function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiMana
|
|
|
94
115
|
<div className="managed-app-actions">
|
|
95
116
|
{app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} commandPreview={action.command_preview} />)}
|
|
96
117
|
</div>
|
|
97
|
-
|
|
118
|
+
</div>
|
|
98
119
|
<div className="managed-app-details">
|
|
99
|
-
<ToggleSwitch checked={app.aidevops_install} label="setup installs" />
|
|
100
|
-
<ToggleSwitch checked={app.aidevops_update} label="update maintains" />
|
|
120
|
+
<ToggleSwitch checked={app.aidevops_install} disabled={lockedPolicy} label="setup installs" onChange={(value) => onPolicyToggle("setup", value)} />
|
|
121
|
+
<ToggleSwitch checked={app.aidevops_update} disabled={lockedPolicy} label="update maintains" onChange={(value) => onPolicyToggle("update", value)} />
|
|
101
122
|
<AppMeta label="Installed" value={app.installed_version} />
|
|
102
123
|
<AppMeta label="Latest" value={app.latest_version} />
|
|
103
124
|
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
104
125
|
{job ? <AppLogLink job={job} /> : null}
|
|
105
126
|
</div>
|
|
106
|
-
{job ? <AppActionTerminal job={job} /> :
|
|
127
|
+
{job ? <AppActionTerminal job={job} /> : null}
|
|
128
|
+
{policyJobs.map((policyJob) => <AppActionTerminal job={policyJob} key={policyJob.id} />)}
|
|
129
|
+
{job === null && policyJobs.length === 0 ? <p className="empty-state compact-notice">No recent command output for this app. Run an action or change a policy toggle to open this app's terminal log.</p> : null}
|
|
107
130
|
</div> : null}
|
|
108
131
|
</article>
|
|
109
132
|
);
|
|
@@ -118,13 +141,11 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
|
|
|
118
141
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
return <a
|
|
144
|
+
return <a href={href} onClick={(event) => openExternalLink(event, href)} rel="noreferrer" target="_blank" title={href}>{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
122
145
|
}
|
|
123
146
|
|
|
124
|
-
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
125
|
-
|
|
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>;
|
|
147
|
+
function ToggleSwitch({ checked, disabled = false, label, onChange }: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }): ReactElement {
|
|
148
|
+
return <button aria-pressed={checked} className={checked ? "managed-toggle checked" : "managed-toggle"} data-tooltip={disabled ? "Essential aidevops component; policy is locked on" : undefined} disabled={disabled} onClick={() => onChange(!checked)} title={disabled ? "Essential aidevops component; policy is locked on" : undefined} type="button"><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span>{label}</button>;
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
|
|
@@ -214,6 +235,9 @@ function managedCategoryForApp(app: GuiManagedAppSummary): ManagedCategoryId {
|
|
|
214
235
|
if (["core", "safety", "automation"].includes(app.category)) {
|
|
215
236
|
return "core";
|
|
216
237
|
}
|
|
238
|
+
if (app.category.includes("cli")) {
|
|
239
|
+
return "cli";
|
|
240
|
+
}
|
|
217
241
|
if (app.category.includes("ai")) {
|
|
218
242
|
return "ai";
|
|
219
243
|
}
|
|
@@ -227,9 +251,17 @@ function managedCategoryForApp(app: GuiManagedAppSummary): ManagedCategoryId {
|
|
|
227
251
|
return "cli";
|
|
228
252
|
}
|
|
229
253
|
|
|
254
|
+
function isEssentialManagedApp(app: GuiManagedAppSummary): boolean {
|
|
255
|
+
return ["core", "safety", "automation"].includes(app.category);
|
|
256
|
+
}
|
|
257
|
+
|
|
230
258
|
type AppCollectionId = "aidevops" | "recommended";
|
|
231
259
|
type ManagedCategoryId = "core" | "ai" | "cli" | "desktop" | "terminal";
|
|
232
260
|
type RecommendedOsId = "all" | "macos" | "linux" | "windows" | "ios" | "android";
|
|
261
|
+
type RecommendedPlatformId = "webapp" | "saas" | "cli" | "api";
|
|
262
|
+
type RecommendedPlatformFilterId = "all" | RecommendedPlatformId;
|
|
263
|
+
type ManagedPolicyId = "setup" | "update";
|
|
264
|
+
type ManagedPolicyToggleState = Record<string, Partial<Record<ManagedPolicyId, boolean>>>;
|
|
233
265
|
|
|
234
266
|
interface TabOption<T extends string> {
|
|
235
267
|
id: T;
|
|
@@ -242,7 +274,12 @@ interface RecommendedApp {
|
|
|
242
274
|
websiteUrl: string;
|
|
243
275
|
alternativeToUrl?: string;
|
|
244
276
|
repoUrl?: string;
|
|
277
|
+
iosUrl?: string;
|
|
278
|
+
androidUrl?: string;
|
|
279
|
+
fdroidUrl?: string;
|
|
280
|
+
obtainiumUrl?: string;
|
|
245
281
|
os: RecommendedOsId[];
|
|
282
|
+
platforms: RecommendedPlatformId[];
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
const appCollectionTabs: TabOption<AppCollectionId>[] = [
|
|
@@ -258,99 +295,209 @@ const managedCategoryTabs: TabOption<ManagedCategoryId>[] = [
|
|
|
258
295
|
{ id: "terminal", label: "Terminal" },
|
|
259
296
|
];
|
|
260
297
|
|
|
261
|
-
const
|
|
298
|
+
const recommendedPlatformTabs: TabOption<RecommendedPlatformFilterId>[] = [
|
|
262
299
|
{ id: "all", label: "All" },
|
|
263
|
-
{ id: "
|
|
264
|
-
{ id: "
|
|
265
|
-
{ id: "
|
|
266
|
-
{ id: "
|
|
267
|
-
{ id: "android", label: "Android" },
|
|
300
|
+
{ id: "webapp", label: "WebApp" },
|
|
301
|
+
{ id: "saas", label: "SaaS" },
|
|
302
|
+
{ id: "cli", label: "CLI" },
|
|
303
|
+
{ id: "api", label: "API" },
|
|
268
304
|
];
|
|
269
305
|
|
|
270
306
|
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: "
|
|
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: "
|
|
293
|
-
{ name: "
|
|
294
|
-
{ name: "
|
|
295
|
-
{ name: "
|
|
296
|
-
{ name: "
|
|
297
|
-
{ name: "
|
|
298
|
-
{ name: "
|
|
299
|
-
{ name: "
|
|
300
|
-
{ name: "
|
|
301
|
-
{ name: "
|
|
302
|
-
{ name: "
|
|
303
|
-
{ name: "
|
|
304
|
-
{ name: "
|
|
305
|
-
{ name: "
|
|
306
|
-
{ name: "
|
|
307
|
-
{ name: "
|
|
308
|
-
{ name: "
|
|
309
|
-
{ name: "
|
|
310
|
-
{ name: "
|
|
311
|
-
{ name: "
|
|
312
|
-
{ name: "
|
|
313
|
-
{ name: "
|
|
314
|
-
{ name: "
|
|
315
|
-
{ name: "
|
|
316
|
-
{ name: "
|
|
307
|
+
{ name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"], platforms: [] },
|
|
308
|
+
{ 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"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
309
|
+
{ name: "Brave Browser", description: "Privacy-focused web browser.", websiteUrl: "https://brave.com/", alternativeToUrl: "https://alternativeto.net/software/brave/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
|
|
310
|
+
{ name: "Cloudron", description: "Self-hosted app platform.", websiteUrl: "https://www.cloudron.io/", alternativeToUrl: "https://alternativeto.net/software/cloudron/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
|
|
311
|
+
{ name: "Collabora Online", description: "Online office collaboration.", websiteUrl: "https://www.collaboraonline.com/collabora-online/", alternativeToUrl: "https://alternativeto.net/software/collabora-online/", os: ["linux"], platforms: ["webapp", "api"] },
|
|
312
|
+
{ name: "Cometly", description: "Marketing attribution analytics.", websiteUrl: "https://www.cometly.com/", os: [], platforms: ["saas", "api"] },
|
|
313
|
+
{ 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"], platforms: [] },
|
|
314
|
+
{ name: "DocuSeal", description: "Open source document signing.", websiteUrl: "https://www.docuseal.com/", alternativeToUrl: "https://alternativeto.net/software/docuseal/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
315
|
+
{ name: "Element", description: "Matrix messaging client.", websiteUrl: "https://element.io/", alternativeToUrl: "https://alternativeto.net/software/element-app/", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp"] },
|
|
316
|
+
{ name: "Enpass", description: "Offline-first password manager.", websiteUrl: "https://www.enpass.io/", alternativeToUrl: "https://alternativeto.net/software/enpass/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
|
|
317
|
+
{ name: "EspoCRM", description: "Open source CRM.", websiteUrl: "https://www.espocrm.com/", alternativeToUrl: "https://alternativeto.net/software/espocrm/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
318
|
+
{ name: "Fathom Analytics", description: "Privacy-first analytics.", websiteUrl: "https://usefathom.com/", alternativeToUrl: "https://alternativeto.net/software/fathom-analytics/", os: [], platforms: ["saas", "api"] },
|
|
319
|
+
{ name: "FontBase", description: "Font management and reference tool.", websiteUrl: "https://fontba.se/", alternativeToUrl: "https://alternativeto.net/software/fontbase/", os: ["macos"], platforms: [] },
|
|
320
|
+
{ name: "Forgejo", description: "Self-hosted Git forge.", websiteUrl: "https://forgejo.org/", alternativeToUrl: "https://alternativeto.net/software/forgejo/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
|
|
321
|
+
{ name: "Ghost", description: "Publishing platform.", websiteUrl: "https://ghost.org/", alternativeToUrl: "https://alternativeto.net/software/ghost/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
322
|
+
{ name: "Gitea", description: "Self-hosted Git service.", websiteUrl: "https://about.gitea.com/", alternativeToUrl: "https://alternativeto.net/software/gitea/", os: ["linux", "windows"], platforms: ["webapp", "cli", "api"] },
|
|
323
|
+
{ name: "GitLab", description: "DevSecOps and Git hosting platform.", websiteUrl: "https://gitlab.com/", alternativeToUrl: "https://alternativeto.net/software/gitlab/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
324
|
+
{ name: "LibreOffice", description: "Open source office suite.", websiteUrl: "https://www.libreoffice.org/", alternativeToUrl: "https://alternativeto.net/software/libreoffice/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
325
|
+
{ name: "LocalWP", description: "Local WordPress development.", websiteUrl: "https://localwp.com/", alternativeToUrl: "https://alternativeto.net/software/local-by-flywheel/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
326
|
+
{ name: "Matomo", description: "Open analytics platform.", websiteUrl: "https://matomo.org/", alternativeToUrl: "https://alternativeto.net/software/piwik/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
327
|
+
{ 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"], platforms: ["webapp", "saas", "api"] },
|
|
328
|
+
{ name: "Nextcloud Talk", description: "Open source video calls, chat, and collaboration for Nextcloud.", websiteUrl: "https://nextcloud.com/talk/", repoUrl: "https://github.com/nextcloud/spreed", iosUrl: "https://apps.apple.com/us/app/nextcloud-talk/id1296825574", androidUrl: "https://play.google.com/store/apps/details?id=com.nextcloud.talk2&hl=en", os: ["ios", "android"], platforms: ["webapp"] },
|
|
329
|
+
{ 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"], platforms: [] },
|
|
330
|
+
{ name: "ONLYOFFICE", description: "Office and document collaboration.", websiteUrl: "https://www.onlyoffice.com/", alternativeToUrl: "https://alternativeto.net/software/onlyoffice/", repoUrl: "https://github.com/ONLYOFFICE/", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "api"] },
|
|
331
|
+
{ name: "OpenScreen", description: "Open screen-sharing project.", websiteUrl: "https://github.com/getopenscreen/openscreen/releases", alternativeToUrl: "https://alternativeto.net/software/openscreen/", repoUrl: "https://github.com/getopenscreen/openscreen", os: ["macos", "linux", "windows"], platforms: [] },
|
|
332
|
+
{ name: "Osaurus", description: "AI app workspace.", websiteUrl: "https://osaurus.ai/", alternativeToUrl: "https://alternativeto.net/software/osaurus/", os: ["macos"], platforms: [] },
|
|
333
|
+
{ 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"], platforms: [] },
|
|
334
|
+
{ name: "PDF Studio", description: "PDF editor.", websiteUrl: "https://www.qoppa.com/pdfstudio/", alternativeToUrl: "https://alternativeto.net/software/qoppa-pdf-studio/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
335
|
+
{ name: "Pixelmator Pro", description: "Professional image editor for macOS.", websiteUrl: "https://www.apple.com/pixelmator-pro/", os: ["macos"], platforms: [] },
|
|
336
|
+
{ name: "PostHog", description: "Product analytics platform.", websiteUrl: "https://posthog.com/", alternativeToUrl: "https://alternativeto.net/software/posthog/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
337
|
+
{ name: "Postiz", description: "Social media scheduling.", websiteUrl: "https://postiz.com/", alternativeToUrl: "https://alternativeto.net/software/postiz/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
338
|
+
{ name: "PrivateBin", description: "Zero-knowledge pastebin.", websiteUrl: "https://privatebin.info/", alternativeToUrl: "https://alternativeto.net/software/privatebin/", repoUrl: "https://github.com/PrivateBin/PrivateBin", os: ["linux"], platforms: ["webapp", "api"] },
|
|
339
|
+
{ name: "Proxmox", description: "Virtualization platform.", websiteUrl: "https://www.proxmox.com/", alternativeToUrl: "https://alternativeto.net/software/proxmox-virtual-environment/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
|
|
340
|
+
{ name: "QuickFile", description: "Accounting platform.", websiteUrl: "https://www.quickfile.co.uk/", os: [], platforms: ["saas", "api"] },
|
|
341
|
+
{ name: "Reframed", description: "Creative framing utility.", websiteUrl: "https://www.reframed.dev/", alternativeToUrl: "https://alternativeto.net/software/reframed/", os: ["macos"], platforms: [] },
|
|
342
|
+
{ name: "Rybbit", description: "Web analytics.", websiteUrl: "https://rybbit.com/", alternativeToUrl: "https://alternativeto.net/software/rybbit/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
|
|
343
|
+
{ name: "SchildiChat", description: "Matrix messaging client.", websiteUrl: "https://schildi.chat/", alternativeToUrl: "https://alternativeto.net/software/schildichat/", os: ["macos", "linux", "windows", "android"], platforms: ["webapp"] },
|
|
344
|
+
{ name: "ScreenFlow", description: "Screen recording and editing.", websiteUrl: "https://www.telestream.net/screenflow/", alternativeToUrl: "https://alternativeto.net/software/screenflow/", os: ["macos", "ios"], platforms: [] },
|
|
345
|
+
{ name: "SEO Utils", description: "Desktop SEO tools.", websiteUrl: "https://seoutils.app/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
346
|
+
{ name: "Shottr", description: "macOS screenshot utility.", websiteUrl: "https://shottr.cc/", alternativeToUrl: "https://alternativeto.net/software/shottr/", os: ["macos"], platforms: [] },
|
|
347
|
+
{ name: "Signal", description: "Private messenger.", websiteUrl: "https://signal.org/", alternativeToUrl: "https://alternativeto.net/software/signal-private-messenger/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
|
|
348
|
+
{ 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"], platforms: ["cli"] },
|
|
349
|
+
{ name: "Telegram", description: "Cloud-based mobile and desktop messaging app.", websiteUrl: "https://telegram.org/", iosUrl: "https://telegram.org/dl/ios", androidUrl: "https://telegram.org/android", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "api"] },
|
|
350
|
+
{ name: "Thunderbird", description: "Email and calendar app.", websiteUrl: "https://www.thunderbird.net/", alternativeToUrl: "https://alternativeto.net/software/mozilla-thunderbird/about/", os: ["macos", "linux", "windows"], platforms: [] },
|
|
351
|
+
{ name: "Ubicloud", description: "Open cloud platform.", websiteUrl: "https://www.ubicloud.com/", alternativeToUrl: "https://alternativeto.net/software/ubicloud/about/", repoUrl: "https://github.com/ubicloud/ubicloud", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
352
|
+
{ 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"], platforms: ["webapp", "api"] },
|
|
353
|
+
{ name: "VideoProc", description: "Video processing toolkit.", websiteUrl: "https://www.videoproc.com/", alternativeToUrl: "https://alternativeto.net/software/videoproc/about/", os: ["macos", "windows"], platforms: [] },
|
|
354
|
+
{ name: "VirtualBox", description: "Virtualization app.", websiteUrl: "https://www.virtualbox.org/", alternativeToUrl: "https://alternativeto.net/software/virtualbox/about/", os: ["macos", "linux", "windows"], platforms: ["cli"] },
|
|
355
|
+
{ name: "WordPress", description: "Open source publishing platform.", websiteUrl: "https://wordpress.org/", alternativeToUrl: "https://alternativeto.net/software/wordpress/about/", os: ["linux"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
317
356
|
] satisfies RecommendedApp[]).sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
318
357
|
|
|
319
358
|
function TabNav<T extends string>({ label, onChange, tabs, value }: { label: string; onChange: (value: T) => void; tabs: TabOption<T>[]; value: T }): ReactElement {
|
|
320
359
|
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
360
|
}
|
|
322
361
|
|
|
323
|
-
function RecommendedAppsSurface({ apps,
|
|
362
|
+
function RecommendedAppsSurface({ apps, platform, setPlatform }: { apps: RecommendedApp[]; platform: RecommendedPlatformFilterId; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
324
363
|
return <>
|
|
325
|
-
<TabNav label="Recommended app
|
|
364
|
+
<TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
|
|
326
365
|
<div className="recommended-app-grid">
|
|
327
|
-
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} />)}
|
|
366
|
+
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} setPlatform={setPlatform} />)}
|
|
328
367
|
</div>
|
|
329
368
|
</>;
|
|
330
369
|
}
|
|
331
370
|
|
|
332
|
-
function RecommendedAppCard({ app }: { app: RecommendedApp }): ReactElement {
|
|
371
|
+
function RecommendedAppCard({ app, setPlatform }: { app: RecommendedApp; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
333
372
|
return <article className="recommended-app-card">
|
|
334
373
|
<div>
|
|
335
374
|
<strong>{app.name}</strong>
|
|
336
375
|
<p>{app.description}</p>
|
|
337
376
|
</div>
|
|
338
|
-
<
|
|
377
|
+
<div className="app-icon-groups">
|
|
378
|
+
<PlatformIconList platforms={app.platforms} setPlatform={setPlatform} />
|
|
379
|
+
<OsIconList os={app.os} />
|
|
380
|
+
</div>
|
|
339
381
|
<div className="managed-app-links plain-links">
|
|
340
382
|
<OriginLink href={app.websiteUrl} label={text.website} />
|
|
341
383
|
{app.alternativeToUrl ? <OriginLink href={app.alternativeToUrl} label="AlternativeTo" /> : null}
|
|
342
384
|
{app.repoUrl ? <OriginLink href={app.repoUrl} label="Repo" /> : null}
|
|
385
|
+
{app.iosUrl ? <OriginLink href={app.iosUrl} label="iOS" /> : null}
|
|
386
|
+
{app.androidUrl ? <OriginLink href={app.androidUrl} label="Android" /> : null}
|
|
387
|
+
{app.fdroidUrl ? <OriginLink href={app.fdroidUrl} label="F-Droid" /> : null}
|
|
388
|
+
{app.obtainiumUrl ? <OriginLink href={app.obtainiumUrl} label="Obtainium" /> : null}
|
|
343
389
|
</div>
|
|
344
390
|
</article>;
|
|
345
391
|
}
|
|
346
392
|
|
|
347
|
-
function
|
|
348
|
-
|
|
393
|
+
function PlatformIconList({ platforms, setPlatform }: { platforms: RecommendedPlatformId[]; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement | null {
|
|
394
|
+
if (platforms.length === 0) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return <span className="os-icon-list platform-icon-list">{platforms.map((platform) => <PlatformIcon id={platform} key={platform} setPlatform={setPlatform} />)}</span>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function PlatformIcon({ id, setPlatform }: { id: RecommendedPlatformId; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
402
|
+
const iconMap: Record<RecommendedPlatformId, { Icon: IconType; label: string }> = {
|
|
403
|
+
webapp: { Icon: FiMonitor, label: "WebApp" },
|
|
404
|
+
saas: { Icon: FiGlobe, label: "SaaS" },
|
|
405
|
+
cli: { Icon: FiTerminal, label: "CLI" },
|
|
406
|
+
api: { Icon: FiCode, label: "API" },
|
|
407
|
+
};
|
|
408
|
+
const { Icon, label } = iconMap[id];
|
|
409
|
+
|
|
410
|
+
return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setPlatform(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function OsIconList({ os }: { os: RecommendedOsId[] }): ReactElement | null {
|
|
349
414
|
if (os.length === 0) {
|
|
350
|
-
return
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return <span className="os-icon-list">{os.map((item) => <OsIcon id={item} key={item} />)}</span>;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function OsIcon({ id }: { id: RecommendedOsId }): ReactElement {
|
|
422
|
+
const iconMap: Record<RecommendedOsId, { Icon: IconType; label: string }> = {
|
|
423
|
+
all: { Icon: FiGlobe, label: "Web app" },
|
|
424
|
+
macos: { Icon: FaApple, label: "macOS" },
|
|
425
|
+
linux: { Icon: FaLinux, label: "Linux" },
|
|
426
|
+
windows: { Icon: FaWindows, label: "Windows" },
|
|
427
|
+
ios: { Icon: SiIos, label: "iOS" },
|
|
428
|
+
android: { Icon: IoLogoAndroid, label: "Android" },
|
|
429
|
+
};
|
|
430
|
+
const { Icon, label } = iconMap[id];
|
|
431
|
+
|
|
432
|
+
return <span aria-label={label} data-tooltip={label} role="img" title={label}><Icon aria-hidden="true" focusable="false" /></span>;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function applyManagedPolicyToggles(app: GuiManagedAppSummary, policy: Partial<Record<ManagedPolicyId, boolean>> | undefined): GuiManagedAppSummary {
|
|
436
|
+
if (policy === undefined) {
|
|
437
|
+
return app;
|
|
351
438
|
}
|
|
352
439
|
|
|
353
|
-
return
|
|
440
|
+
return {
|
|
441
|
+
...app,
|
|
442
|
+
aidevops_install: policy.setup ?? app.aidevops_install,
|
|
443
|
+
aidevops_update: policy.update ?? app.aidevops_update,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function createPolicyJob(appId: string, policy: ManagedPolicyId, value: boolean): GuiAppActionJobSummary {
|
|
448
|
+
const mode = value ? "on" : "off";
|
|
449
|
+
const command = `aidevops app policy set ${appId} --${policy === "setup" ? "setup-installs" : "update-maintains"} ${mode}`;
|
|
450
|
+
const now = new Date().toISOString();
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
id: `policy-${appId}-${policy}-${Date.now()}`,
|
|
454
|
+
app_id: appId,
|
|
455
|
+
action: "update",
|
|
456
|
+
status: "completed",
|
|
457
|
+
command_preview: command,
|
|
458
|
+
started_at: now,
|
|
459
|
+
finished_at: now,
|
|
460
|
+
exit_code: 0,
|
|
461
|
+
output: [
|
|
462
|
+
`$ ${command}`,
|
|
463
|
+
`Saved local aidevops.app policy override for ${appId}: ${policy}=${mode}.`,
|
|
464
|
+
"Persistence: browser localStorage key aidevops-gui-managed-app-policy.",
|
|
465
|
+
],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function readManagedPolicyToggles(): ManagedPolicyToggleState {
|
|
470
|
+
if (typeof window === "undefined") {
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const raw = window.localStorage.getItem("aidevops-gui-managed-app-policy");
|
|
476
|
+
return raw === null ? {} : JSON.parse(raw) as ManagedPolicyToggleState;
|
|
477
|
+
} catch {
|
|
478
|
+
return {};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function writeManagedPolicyToggles(policy: ManagedPolicyToggleState): void {
|
|
483
|
+
if (typeof window === "undefined") {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
window.localStorage.setItem("aidevops-gui-managed-app-policy", JSON.stringify(policy));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function openExternalLink(event: ReactMouseEvent<HTMLAnchorElement>, href: string): void {
|
|
491
|
+
event.stopPropagation();
|
|
492
|
+
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const opened = window.open(href, "_blank", "noopener,noreferrer");
|
|
497
|
+
if (opened !== null) {
|
|
498
|
+
event.preventDefault();
|
|
499
|
+
opened.opener = null;
|
|
500
|
+
}
|
|
354
501
|
}
|
|
355
502
|
|
|
356
503
|
export function InstallationSurface(): ReactElement {
|
|
@@ -335,6 +335,19 @@ function plannedCommsGroup(label: string, items: SurfaceNavItem[]): SurfaceNavGr
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
export const navGroups: SurfaceNavGroup[] = [
|
|
338
|
+
{
|
|
339
|
+
label: text.infrastructure,
|
|
340
|
+
mode: "devops",
|
|
341
|
+
items: [
|
|
342
|
+
{ id: "devices", label: text.devices, description: "Installed aidevops machines", icon: "device", badge: text.planned },
|
|
343
|
+
{ id: "apps", label: text.apps, description: "Installed tools and apps", icon: "package" },
|
|
344
|
+
{ id: "vpnsProxies", label: text.vpnsProxies, description: "VPN and proxy inventory", icon: "shield", badge: text.planned },
|
|
345
|
+
{ id: "installation", label: text.installation, description: "Optional setup toggles", icon: "download" },
|
|
346
|
+
{ id: "registrars", label: text.registrars, description: "Domain registrars", icon: "chain" },
|
|
347
|
+
{ id: "hosts", label: text.hosts, description: "Hosting providers", icon: "server" },
|
|
348
|
+
{ id: "servers", label: text.servers, description: "Servers and orchestrators", icon: "hardDrive" },
|
|
349
|
+
],
|
|
350
|
+
},
|
|
338
351
|
{
|
|
339
352
|
label: text.development,
|
|
340
353
|
mode: "devops",
|
|
@@ -360,19 +373,6 @@ export const navGroups: SurfaceNavGroup[] = [
|
|
|
360
373
|
{ id: "routines", label: text.routines, description: "Scheduled workflows", icon: "clock", badge: text.planned },
|
|
361
374
|
],
|
|
362
375
|
},
|
|
363
|
-
{
|
|
364
|
-
label: text.infrastructure,
|
|
365
|
-
mode: "devops",
|
|
366
|
-
items: [
|
|
367
|
-
{ id: "devices", label: text.devices, description: "Installed aidevops machines", icon: "device", badge: text.planned },
|
|
368
|
-
{ id: "vpnsProxies", label: text.vpnsProxies, description: "VPN and proxy inventory", icon: "shield", badge: text.planned },
|
|
369
|
-
{ id: "apps", label: text.apps, description: "Installed tools and apps", icon: "package" },
|
|
370
|
-
{ id: "installation", label: text.installation, description: "Optional setup toggles", icon: "download" },
|
|
371
|
-
{ id: "registrars", label: text.registrars, description: "Domain registrars", icon: "chain" },
|
|
372
|
-
{ id: "hosts", label: text.hosts, description: "Hosting providers", icon: "server" },
|
|
373
|
-
{ id: "servers", label: text.servers, description: "Servers and orchestrators", icon: "hardDrive" },
|
|
374
|
-
],
|
|
375
|
-
},
|
|
376
376
|
{
|
|
377
377
|
label: text.identities,
|
|
378
378
|
mode: "comms",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--accent-hue: 123;
|
|
3
3
|
--accent: hsl(var(--accent-hue) 50% 36%);
|
|
4
|
+
--accent-aaa-bg: hsl(var(--accent-hue) 58% 24%);
|
|
4
5
|
--accent-strong: hsl(var(--accent-hue) 56% 28%);
|
|
5
6
|
--bg-primary: #ffffff;
|
|
6
7
|
--bg-secondary: #f8f8f8;
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
|
|
34
35
|
:root[data-theme="dark"] {
|
|
35
36
|
--accent: hsl(var(--accent-hue) 74% 66%);
|
|
37
|
+
--accent-aaa-bg: hsl(var(--accent-hue) 64% 24%);
|
|
36
38
|
--accent-strong: hsl(var(--accent-hue) 75% 72%);
|
|
37
39
|
--bg-primary: #0a0a0a;
|
|
38
40
|
--bg-secondary: #111111;
|
|
@@ -921,6 +923,15 @@ code {
|
|
|
921
923
|
min-width: 0;
|
|
922
924
|
}
|
|
923
925
|
|
|
926
|
+
.header-actions > .header-icon-button,
|
|
927
|
+
.header-actions > .header-action-menu {
|
|
928
|
+
align-items: center;
|
|
929
|
+
display: inline-flex;
|
|
930
|
+
flex: 0 0 38px;
|
|
931
|
+
justify-content: center;
|
|
932
|
+
width: 38px;
|
|
933
|
+
}
|
|
934
|
+
|
|
924
935
|
.workspace-search {
|
|
925
936
|
background: var(--surface-muted);
|
|
926
937
|
border: 1px solid var(--border);
|
|
@@ -1966,7 +1977,12 @@ code {
|
|
|
1966
1977
|
|
|
1967
1978
|
.apps-surface {
|
|
1968
1979
|
display: grid;
|
|
1969
|
-
|
|
1980
|
+
align-content: start;
|
|
1981
|
+
gap: 10px;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.apps-surface .section-heading {
|
|
1985
|
+
margin-bottom: 8px;
|
|
1970
1986
|
}
|
|
1971
1987
|
|
|
1972
1988
|
.data-table,
|
|
@@ -1990,13 +2006,21 @@ code {
|
|
|
1990
2006
|
border: 1px solid var(--border);
|
|
1991
2007
|
border-radius: 18px;
|
|
1992
2008
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
|
|
1993
|
-
overflow:
|
|
2009
|
+
overflow: hidden;
|
|
2010
|
+
transition: background 140ms ease, border-color 140ms ease;
|
|
1994
2011
|
}
|
|
1995
2012
|
|
|
1996
2013
|
.managed-app-card.expanded {
|
|
1997
2014
|
border-color: color-mix(in srgb, var(--accent) 32%, var(--border));
|
|
1998
2015
|
}
|
|
1999
2016
|
|
|
2017
|
+
.managed-app-card:hover,
|
|
2018
|
+
.managed-app-card:focus-within {
|
|
2019
|
+
background:
|
|
2020
|
+
linear-gradient(135deg, color-mix(in srgb, var(--accent) 11%, transparent), transparent 40%),
|
|
2021
|
+
var(--surface-muted);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2000
2024
|
.managed-app-summary {
|
|
2001
2025
|
align-items: center;
|
|
2002
2026
|
background: transparent;
|
|
@@ -2006,6 +2030,7 @@ code {
|
|
|
2006
2030
|
gap: 16px;
|
|
2007
2031
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
2008
2032
|
padding: 16px;
|
|
2033
|
+
position: relative;
|
|
2009
2034
|
text-align: left;
|
|
2010
2035
|
width: 100%;
|
|
2011
2036
|
}
|
|
@@ -2089,6 +2114,7 @@ code {
|
|
|
2089
2114
|
display: grid;
|
|
2090
2115
|
gap: 14px;
|
|
2091
2116
|
padding: 0 16px 16px;
|
|
2117
|
+
position: relative;
|
|
2092
2118
|
}
|
|
2093
2119
|
|
|
2094
2120
|
.managed-app-toolbar {
|
|
@@ -2116,7 +2142,7 @@ code {
|
|
|
2116
2142
|
align-items: center;
|
|
2117
2143
|
color: var(--text-secondary);
|
|
2118
2144
|
display: inline-flex;
|
|
2119
|
-
font-size: 0.
|
|
2145
|
+
font-size: 0.95em;
|
|
2120
2146
|
font-weight: 800;
|
|
2121
2147
|
gap: 4px;
|
|
2122
2148
|
max-width: 100%;
|
|
@@ -2167,6 +2193,11 @@ button.managed-toggle {
|
|
|
2167
2193
|
text-align: left;
|
|
2168
2194
|
}
|
|
2169
2195
|
|
|
2196
|
+
button.managed-toggle:disabled {
|
|
2197
|
+
cursor: not-allowed;
|
|
2198
|
+
opacity: 0.82;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2170
2201
|
button.managed-toggle:hover,
|
|
2171
2202
|
button.managed-toggle:focus-visible {
|
|
2172
2203
|
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
|
@@ -2363,7 +2394,7 @@ button.managed-toggle:focus-visible {
|
|
|
2363
2394
|
content: attr(data-tooltip);
|
|
2364
2395
|
font-size: 0.72rem;
|
|
2365
2396
|
font-weight: 700;
|
|
2366
|
-
left: 50
|
|
2397
|
+
left: clamp(10px, 50%, calc(100vw - 330px));
|
|
2367
2398
|
line-height: 1.35;
|
|
2368
2399
|
max-width: min(320px, 72vw);
|
|
2369
2400
|
opacity: 0;
|
|
@@ -2374,7 +2405,7 @@ button.managed-toggle:focus-visible {
|
|
|
2374
2405
|
transform: translate(-50%, 4px);
|
|
2375
2406
|
transition: opacity 140ms ease, transform 140ms ease;
|
|
2376
2407
|
width: max-content;
|
|
2377
|
-
z-index:
|
|
2408
|
+
z-index: 400;
|
|
2378
2409
|
}
|
|
2379
2410
|
|
|
2380
2411
|
[data-tooltip]:hover::after,
|
|
@@ -2453,14 +2484,16 @@ button.managed-toggle:focus-visible {
|
|
|
2453
2484
|
background: var(--surface-muted);
|
|
2454
2485
|
border: 1px solid var(--border);
|
|
2455
2486
|
border-radius: 999px;
|
|
2456
|
-
|
|
2487
|
+
align-items: center;
|
|
2488
|
+
display: inline-flex;
|
|
2457
2489
|
gap: 4px;
|
|
2458
2490
|
padding: 5px;
|
|
2459
|
-
width:
|
|
2491
|
+
width: max-content;
|
|
2492
|
+
max-width: 100%;
|
|
2460
2493
|
}
|
|
2461
2494
|
|
|
2462
2495
|
.app-subnav {
|
|
2463
|
-
|
|
2496
|
+
justify-self: start;
|
|
2464
2497
|
}
|
|
2465
2498
|
|
|
2466
2499
|
.recommended-app-grid {
|
|
@@ -2495,19 +2528,38 @@ button.managed-toggle:focus-visible {
|
|
|
2495
2528
|
gap: 6px;
|
|
2496
2529
|
}
|
|
2497
2530
|
|
|
2498
|
-
.
|
|
2531
|
+
.app-icon-groups {
|
|
2532
|
+
display: flex;
|
|
2533
|
+
flex-wrap: wrap;
|
|
2534
|
+
gap: 10px;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
.os-icon-list span,
|
|
2538
|
+
.os-icon-list button {
|
|
2499
2539
|
align-items: center;
|
|
2500
|
-
background:
|
|
2501
|
-
border:
|
|
2502
|
-
border-radius:
|
|
2540
|
+
background: transparent;
|
|
2541
|
+
border: 0;
|
|
2542
|
+
border-radius: 0;
|
|
2503
2543
|
color: var(--text-secondary);
|
|
2504
2544
|
display: inline-flex;
|
|
2505
|
-
font-size:
|
|
2545
|
+
font-size: 1.12rem;
|
|
2506
2546
|
font-weight: 900;
|
|
2507
2547
|
justify-content: center;
|
|
2508
|
-
min-height:
|
|
2509
|
-
min-width:
|
|
2510
|
-
padding:
|
|
2548
|
+
min-height: 24px;
|
|
2549
|
+
min-width: 24px;
|
|
2550
|
+
padding: 0;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
.platform-icon-list button {
|
|
2554
|
+
cursor: pointer;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
.os-icon-list span:hover,
|
|
2558
|
+
.os-icon-list span:focus-visible,
|
|
2559
|
+
.os-icon-list button:hover,
|
|
2560
|
+
.os-icon-list button:focus-visible {
|
|
2561
|
+
color: var(--accent);
|
|
2562
|
+
outline: none;
|
|
2511
2563
|
}
|
|
2512
2564
|
|
|
2513
2565
|
.pill-tabs button {
|
|
@@ -2515,20 +2567,30 @@ button.managed-toggle:focus-visible {
|
|
|
2515
2567
|
border: 0;
|
|
2516
2568
|
border-radius: 999px;
|
|
2517
2569
|
color: var(--text-secondary);
|
|
2518
|
-
flex:
|
|
2570
|
+
flex: 0 0 auto;
|
|
2519
2571
|
font-weight: 800;
|
|
2520
2572
|
min-height: 38px;
|
|
2521
2573
|
padding: 9px 14px;
|
|
2522
2574
|
}
|
|
2523
2575
|
|
|
2524
|
-
.pill-tabs button.active
|
|
2576
|
+
.pill-tabs button.active {
|
|
2577
|
+
background: var(--accent-aaa-bg);
|
|
2578
|
+
color: #ffffff;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2525
2581
|
.pill-tabs button:hover,
|
|
2526
2582
|
.pill-tabs button:focus-visible {
|
|
2527
|
-
background: var(--surface-elevated);
|
|
2528
|
-
color: var(--
|
|
2583
|
+
background: color-mix(in srgb, var(--accent) 16%, var(--surface-elevated));
|
|
2584
|
+
color: var(--accent);
|
|
2529
2585
|
outline: none;
|
|
2530
2586
|
}
|
|
2531
2587
|
|
|
2588
|
+
.pill-tabs button.active:hover,
|
|
2589
|
+
.pill-tabs button.active:focus-visible {
|
|
2590
|
+
background: var(--accent-aaa-bg);
|
|
2591
|
+
color: #ffffff;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2532
2594
|
.count-pill,
|
|
2533
2595
|
.repo-summary-strip span {
|
|
2534
2596
|
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
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.11
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|