aidevops 3.29.8 → 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 +369 -49
- package/packages/gui-web/src/app-model.ts +13 -13
- package/packages/gui-web/src/styles.css +203 -19
- 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, 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
|
|
|
@@ -12,17 +16,22 @@ interface DraftInventoryRow {
|
|
|
12
16
|
let draftRowCounter = 0;
|
|
13
17
|
|
|
14
18
|
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
19
|
+
const [appCollection, setAppCollection] = useState<AppCollectionId>("aidevops");
|
|
20
|
+
const [managedCategory, setManagedCategory] = useState<ManagedCategoryId>("core");
|
|
21
|
+
const [recommendedPlatform, setRecommendedPlatform] = useState<RecommendedPlatformFilterId>("all");
|
|
15
22
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
16
|
-
const [
|
|
23
|
+
const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
|
|
17
24
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
25
|
+
const [policyJobs, setPolicyJobs] = useState<Record<string, GuiAppActionJobSummary[]>>({});
|
|
26
|
+
const [policyToggles, setPolicyToggles] = useState<ManagedPolicyToggleState>(() => readManagedPolicyToggles());
|
|
18
27
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
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));
|
|
19
31
|
|
|
20
32
|
useEffect(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
setExpandedAppIds((current) => current.size === 0 ? new Set([status.managed_apps[0].id]) : current);
|
|
25
|
-
}, [status.managed_apps]);
|
|
33
|
+
setExpandedAppId(visibleManagedApps[0]?.id ?? null);
|
|
34
|
+
}, [managedCategory, status.managed_apps]);
|
|
26
35
|
|
|
27
36
|
useEffect(() => {
|
|
28
37
|
if (selectedJob === null || selectedJob.status !== "running") {
|
|
@@ -37,34 +46,52 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
37
46
|
}, [selectedJob]);
|
|
38
47
|
|
|
39
48
|
return (
|
|
40
|
-
<section className="
|
|
49
|
+
<section className="apps-surface" aria-label={text.apps}>
|
|
41
50
|
<div className="section-heading">
|
|
42
51
|
<p className="eyebrow">{text.infrastructure}</p>
|
|
43
52
|
<h2>{text.apps}</h2>
|
|
44
53
|
<p>{text.appsIntro}</p>
|
|
45
54
|
</div>
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
<TabNav label="Apps collections" tabs={appCollectionTabs} value={appCollection} onChange={(value) => setAppCollection(value)} />
|
|
56
|
+
{appCollection === "aidevops" ? <>
|
|
57
|
+
<TabNav label="aidevops app groups" tabs={managedCategoryTabs} value={managedCategory} onChange={(value) => setManagedCategory(value)} />
|
|
58
|
+
<div className="managed-app-list">
|
|
59
|
+
{visibleManagedApps.map((app) => (
|
|
60
|
+
<ManagedAppPanel
|
|
61
|
+
app={app}
|
|
62
|
+
expanded={expandedAppId === app.id}
|
|
63
|
+
job={jobForApp(app.id, jobs, selectedJob)}
|
|
64
|
+
key={app.id}
|
|
65
|
+
policyJobs={policyJobs[app.id] ?? []}
|
|
66
|
+
onJob={(job) => {
|
|
67
|
+
setJobs((current) => ({ ...current, [job.id]: job }));
|
|
68
|
+
setSelectedJobId(job.id);
|
|
69
|
+
setExpandedAppId(app.id);
|
|
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
|
+
}}
|
|
82
|
+
onToggle={() => setExpandedAppId((current) => current === app.id ? null : app.id)}
|
|
83
|
+
/>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
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>
|
|
87
|
+
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} platform={recommendedPlatform} setPlatform={setRecommendedPlatform} />}
|
|
63
88
|
</section>
|
|
64
89
|
);
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
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
|
+
|
|
68
95
|
return (
|
|
69
96
|
<article className={expanded ? "managed-app-card expanded" : "managed-app-card"}>
|
|
70
97
|
<button aria-expanded={expanded} className="managed-app-summary" data-tooltip={`${expanded ? "Collapse" : "Expand"} ${app.name} controls`} onClick={onToggle} type="button">
|
|
@@ -74,7 +101,6 @@ function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiMana
|
|
|
74
101
|
<span>{app.description}</span>
|
|
75
102
|
</span>
|
|
76
103
|
<span className="managed-app-summary-meta">
|
|
77
|
-
<SummaryChip label="Status" value={app.status} />
|
|
78
104
|
<SummaryChip label="Installed" value={app.installed_version} />
|
|
79
105
|
<SummaryChip label="Latest" value={app.latest_version} />
|
|
80
106
|
<FiChevronDown aria-hidden="true" className="managed-app-chevron" />
|
|
@@ -89,16 +115,18 @@ function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiMana
|
|
|
89
115
|
<div className="managed-app-actions">
|
|
90
116
|
{app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} commandPreview={action.command_preview} />)}
|
|
91
117
|
</div>
|
|
92
|
-
|
|
118
|
+
</div>
|
|
93
119
|
<div className="managed-app-details">
|
|
94
|
-
<ToggleSwitch checked={app.aidevops_install} label="setup installs" />
|
|
95
|
-
<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)} />
|
|
96
122
|
<AppMeta label="Installed" value={app.installed_version} />
|
|
97
123
|
<AppMeta label="Latest" value={app.latest_version} />
|
|
98
124
|
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
99
|
-
<
|
|
125
|
+
{job ? <AppLogLink job={job} /> : null}
|
|
100
126
|
</div>
|
|
101
|
-
{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}
|
|
102
130
|
</div> : null}
|
|
103
131
|
</article>
|
|
104
132
|
);
|
|
@@ -113,41 +141,71 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
|
|
|
113
141
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
114
142
|
}
|
|
115
143
|
|
|
116
|
-
return <a
|
|
144
|
+
return <a href={href} onClick={(event) => openExternalLink(event, href)} rel="noreferrer" target="_blank" title={href}>{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
117
145
|
}
|
|
118
146
|
|
|
119
|
-
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
120
|
-
return <
|
|
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>;
|
|
121
149
|
}
|
|
122
150
|
|
|
123
151
|
function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
|
|
124
152
|
return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
|
|
125
153
|
}
|
|
126
154
|
|
|
155
|
+
function AppLogLink({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
156
|
+
return <a className="app-meta app-log-link" href={`#app-job-${job.id}`}><small>Logs</small><strong>{job.action} · {job.status}</strong></a>;
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
function AppActionButton({ action, app, commandPreview, disabled, onJob }: { action: GuiAppActionId; app: GuiManagedAppSummary; commandPreview: string; disabled: boolean; onJob: (job: GuiAppActionJobSummary) => void }): ReactElement {
|
|
128
160
|
const icon = action === "install" ? <FiDownload /> : action === "update" ? <FiRefreshCw /> : action === "reinstall" ? <FiRepeat /> : <FiTrash2 />;
|
|
161
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
129
162
|
|
|
130
163
|
async function runAction(): Promise<void> {
|
|
131
164
|
if (disabled) {
|
|
132
165
|
return;
|
|
133
166
|
}
|
|
134
|
-
if ((action === "remove" || action === "reinstall") && !window.confirm(`${action} ${app.name}?`)) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
167
|
|
|
138
168
|
const response = await fetch(`/api/apps/${encodeURIComponent(app.id)}/actions/${action}`, { method: "POST" });
|
|
139
169
|
const envelope = await response.json() as GuiResponseEnvelope<GuiAppActionJobSummary>;
|
|
140
170
|
onJob(envelope.data);
|
|
141
171
|
}
|
|
142
172
|
|
|
143
|
-
return
|
|
173
|
+
return <>
|
|
174
|
+
<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>
|
|
175
|
+
{confirmOpen ? <ConfirmActionModal action={action} app={app} commandPreview={commandPreview} close={() => setConfirmOpen(false)} confirm={() => { setConfirmOpen(false); void runAction(); }} /> : null}
|
|
176
|
+
</>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ConfirmActionModal({ action, app, close, commandPreview, confirm }: { action: GuiAppActionId; app: GuiManagedAppSummary; close: () => void; commandPreview: string; confirm: () => void }): ReactElement {
|
|
180
|
+
return (
|
|
181
|
+
<div className="modal-backdrop" role="presentation">
|
|
182
|
+
<section aria-modal="true" className="confirm-modal" role="dialog" aria-labelledby={`confirm-${app.id}-${action}`}>
|
|
183
|
+
<p className="eyebrow">Confirm action</p>
|
|
184
|
+
<h3 id={`confirm-${app.id}-${action}`}>{action} {app.name}?</h3>
|
|
185
|
+
<p>This will start the allowlisted background command for this app.</p>
|
|
186
|
+
<code>{commandPreview}</code>
|
|
187
|
+
<div className="confirm-modal-actions">
|
|
188
|
+
<button className="secondary-action" onClick={close} type="button">Cancel</button>
|
|
189
|
+
<button className={action === "remove" ? "app-action-button remove" : "app-action-button"} onClick={confirm} type="button">Confirm</button>
|
|
190
|
+
</div>
|
|
191
|
+
</section>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
144
194
|
}
|
|
145
195
|
|
|
146
196
|
function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
197
|
+
const outputRef = useRef<HTMLPreElement | null>(null);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (outputRef.current !== null) {
|
|
201
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
202
|
+
}
|
|
203
|
+
}, [job.output]);
|
|
204
|
+
|
|
147
205
|
return (
|
|
148
|
-
<section className="app-terminal" aria-label="App action terminal output">
|
|
206
|
+
<section className="app-terminal" id={`app-job-${job.id}`} aria-label="App action terminal output">
|
|
149
207
|
<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>
|
|
208
|
+
<pre ref={outputRef}>{job.output.join("\n")}</pre>
|
|
151
209
|
</section>
|
|
152
210
|
);
|
|
153
211
|
}
|
|
@@ -169,15 +227,277 @@ function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>,
|
|
|
169
227
|
return Object.values(jobs).reverse().find((job) => job.app_id === appId) ?? null;
|
|
170
228
|
}
|
|
171
229
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
230
|
+
function sortedManagedApps(apps: GuiManagedAppSummary[]): GuiManagedAppSummary[] {
|
|
231
|
+
return [...apps].sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function managedCategoryForApp(app: GuiManagedAppSummary): ManagedCategoryId {
|
|
235
|
+
if (["core", "safety", "automation"].includes(app.category)) {
|
|
236
|
+
return "core";
|
|
237
|
+
}
|
|
238
|
+
if (app.category.includes("cli")) {
|
|
239
|
+
return "cli";
|
|
240
|
+
}
|
|
241
|
+
if (app.category.includes("ai")) {
|
|
242
|
+
return "ai";
|
|
243
|
+
}
|
|
244
|
+
if (["desktop", "editor"].includes(app.category)) {
|
|
245
|
+
return "desktop";
|
|
246
|
+
}
|
|
247
|
+
if (["terminal"].includes(app.category) || app.name.toLowerCase().includes("terminal")) {
|
|
248
|
+
return "terminal";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return "cli";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isEssentialManagedApp(app: GuiManagedAppSummary): boolean {
|
|
255
|
+
return ["core", "safety", "automation"].includes(app.category);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
type AppCollectionId = "aidevops" | "recommended";
|
|
259
|
+
type ManagedCategoryId = "core" | "ai" | "cli" | "desktop" | "terminal";
|
|
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>>>;
|
|
265
|
+
|
|
266
|
+
interface TabOption<T extends string> {
|
|
267
|
+
id: T;
|
|
268
|
+
label: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface RecommendedApp {
|
|
272
|
+
name: string;
|
|
273
|
+
description: string;
|
|
274
|
+
websiteUrl: string;
|
|
275
|
+
alternativeToUrl?: string;
|
|
276
|
+
repoUrl?: string;
|
|
277
|
+
iosUrl?: string;
|
|
278
|
+
androidUrl?: string;
|
|
279
|
+
fdroidUrl?: string;
|
|
280
|
+
obtainiumUrl?: string;
|
|
281
|
+
os: RecommendedOsId[];
|
|
282
|
+
platforms: RecommendedPlatformId[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const appCollectionTabs: TabOption<AppCollectionId>[] = [
|
|
286
|
+
{ id: "aidevops", label: "aidevops" },
|
|
287
|
+
{ id: "recommended", label: "Recommended" },
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const managedCategoryTabs: TabOption<ManagedCategoryId>[] = [
|
|
291
|
+
{ id: "core", label: "Core" },
|
|
292
|
+
{ id: "ai", label: "AI" },
|
|
293
|
+
{ id: "cli", label: "CLI" },
|
|
294
|
+
{ id: "desktop", label: "Desktop" },
|
|
295
|
+
{ id: "terminal", label: "Terminal" },
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
const recommendedPlatformTabs: TabOption<RecommendedPlatformFilterId>[] = [
|
|
299
|
+
{ id: "all", label: "All" },
|
|
300
|
+
{ id: "webapp", label: "WebApp" },
|
|
301
|
+
{ id: "saas", label: "SaaS" },
|
|
302
|
+
{ id: "cli", label: "CLI" },
|
|
303
|
+
{ id: "api", label: "API" },
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const recommendedApps = ([
|
|
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"] },
|
|
356
|
+
] satisfies RecommendedApp[]).sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
357
|
+
|
|
358
|
+
function TabNav<T extends string>({ label, onChange, tabs, value }: { label: string; onChange: (value: T) => void; tabs: TabOption<T>[]; value: T }): ReactElement {
|
|
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>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function RecommendedAppsSurface({ apps, platform, setPlatform }: { apps: RecommendedApp[]; platform: RecommendedPlatformFilterId; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
363
|
+
return <>
|
|
364
|
+
<TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
|
|
365
|
+
<div className="recommended-app-grid">
|
|
366
|
+
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} setPlatform={setPlatform} />)}
|
|
367
|
+
</div>
|
|
368
|
+
</>;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function RecommendedAppCard({ app, setPlatform }: { app: RecommendedApp; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
372
|
+
return <article className="recommended-app-card">
|
|
373
|
+
<div>
|
|
374
|
+
<strong>{app.name}</strong>
|
|
375
|
+
<p>{app.description}</p>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="app-icon-groups">
|
|
378
|
+
<PlatformIconList platforms={app.platforms} setPlatform={setPlatform} />
|
|
379
|
+
<OsIconList os={app.os} />
|
|
380
|
+
</div>
|
|
381
|
+
<div className="managed-app-links plain-links">
|
|
382
|
+
<OriginLink href={app.websiteUrl} label={text.website} />
|
|
383
|
+
{app.alternativeToUrl ? <OriginLink href={app.alternativeToUrl} label="AlternativeTo" /> : null}
|
|
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}
|
|
389
|
+
</div>
|
|
390
|
+
</article>;
|
|
391
|
+
}
|
|
392
|
+
|
|
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 {
|
|
414
|
+
if (os.length === 0) {
|
|
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;
|
|
178
438
|
}
|
|
179
439
|
|
|
180
|
-
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
|
+
}
|
|
181
501
|
}
|
|
182
502
|
|
|
183
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);
|
|
@@ -1964,6 +1975,16 @@ code {
|
|
|
1964
1975
|
margin-bottom: 12px;
|
|
1965
1976
|
}
|
|
1966
1977
|
|
|
1978
|
+
.apps-surface {
|
|
1979
|
+
display: grid;
|
|
1980
|
+
align-content: start;
|
|
1981
|
+
gap: 10px;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.apps-surface .section-heading {
|
|
1985
|
+
margin-bottom: 8px;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1967
1988
|
.data-table,
|
|
1968
1989
|
.editable-table,
|
|
1969
1990
|
.installation-list,
|
|
@@ -1985,13 +2006,21 @@ code {
|
|
|
1985
2006
|
border: 1px solid var(--border);
|
|
1986
2007
|
border-radius: 18px;
|
|
1987
2008
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
|
|
1988
|
-
overflow:
|
|
2009
|
+
overflow: hidden;
|
|
2010
|
+
transition: background 140ms ease, border-color 140ms ease;
|
|
1989
2011
|
}
|
|
1990
2012
|
|
|
1991
2013
|
.managed-app-card.expanded {
|
|
1992
2014
|
border-color: color-mix(in srgb, var(--accent) 32%, var(--border));
|
|
1993
2015
|
}
|
|
1994
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
|
+
|
|
1995
2024
|
.managed-app-summary {
|
|
1996
2025
|
align-items: center;
|
|
1997
2026
|
background: transparent;
|
|
@@ -2001,6 +2030,7 @@ code {
|
|
|
2001
2030
|
gap: 16px;
|
|
2002
2031
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
2003
2032
|
padding: 16px;
|
|
2033
|
+
position: relative;
|
|
2004
2034
|
text-align: left;
|
|
2005
2035
|
width: 100%;
|
|
2006
2036
|
}
|
|
@@ -2039,13 +2069,16 @@ code {
|
|
|
2039
2069
|
}
|
|
2040
2070
|
|
|
2041
2071
|
.managed-summary-chip {
|
|
2072
|
+
align-items: center;
|
|
2042
2073
|
background: color-mix(in srgb, var(--surface-elevated) 92%, transparent);
|
|
2043
2074
|
border: 1px solid var(--border);
|
|
2044
2075
|
border-radius: 999px;
|
|
2045
2076
|
display: inline-grid;
|
|
2046
2077
|
gap: 1px;
|
|
2078
|
+
justify-items: center;
|
|
2047
2079
|
min-width: 118px;
|
|
2048
2080
|
padding: 7px 11px;
|
|
2081
|
+
text-align: center;
|
|
2049
2082
|
}
|
|
2050
2083
|
|
|
2051
2084
|
.managed-summary-chip small {
|
|
@@ -2081,6 +2114,7 @@ code {
|
|
|
2081
2114
|
display: grid;
|
|
2082
2115
|
gap: 14px;
|
|
2083
2116
|
padding: 0 16px 16px;
|
|
2117
|
+
position: relative;
|
|
2084
2118
|
}
|
|
2085
2119
|
|
|
2086
2120
|
.managed-app-toolbar {
|
|
@@ -2106,25 +2140,22 @@ code {
|
|
|
2106
2140
|
.managed-app-links a,
|
|
2107
2141
|
.origin-missing {
|
|
2108
2142
|
align-items: center;
|
|
2109
|
-
border: 1px solid var(--border);
|
|
2110
|
-
border-radius: 999px;
|
|
2111
2143
|
color: var(--text-secondary);
|
|
2112
2144
|
display: inline-flex;
|
|
2113
|
-
font-size: 0.
|
|
2145
|
+
font-size: 0.95em;
|
|
2114
2146
|
font-weight: 800;
|
|
2115
2147
|
gap: 4px;
|
|
2116
2148
|
max-width: 100%;
|
|
2117
|
-
padding:
|
|
2149
|
+
padding: 4px 0;
|
|
2118
2150
|
position: relative;
|
|
2119
2151
|
text-decoration: none;
|
|
2120
2152
|
}
|
|
2121
2153
|
|
|
2122
2154
|
.managed-app-links a:hover,
|
|
2123
2155
|
.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
2156
|
color: var(--accent);
|
|
2127
2157
|
outline: none;
|
|
2158
|
+
text-decoration: underline;
|
|
2128
2159
|
}
|
|
2129
2160
|
|
|
2130
2161
|
.origin-missing {
|
|
@@ -2157,9 +2188,25 @@ code {
|
|
|
2157
2188
|
text-transform: uppercase;
|
|
2158
2189
|
}
|
|
2159
2190
|
|
|
2191
|
+
button.managed-toggle {
|
|
2192
|
+
cursor: pointer;
|
|
2193
|
+
text-align: left;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
button.managed-toggle:disabled {
|
|
2197
|
+
cursor: not-allowed;
|
|
2198
|
+
opacity: 0.82;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
button.managed-toggle:hover,
|
|
2202
|
+
button.managed-toggle:focus-visible {
|
|
2203
|
+
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
|
2204
|
+
outline: none;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2160
2207
|
.switch-track {
|
|
2161
|
-
background: var(--surface-muted);
|
|
2162
|
-
border: 1px solid var(--border);
|
|
2208
|
+
background: color-mix(in srgb, #ef4444 18%, var(--surface-muted));
|
|
2209
|
+
border: 1px solid color-mix(in srgb, #ef4444 44%, var(--border));
|
|
2163
2210
|
border-radius: 999px;
|
|
2164
2211
|
display: inline-flex;
|
|
2165
2212
|
padding: 2px;
|
|
@@ -2167,7 +2214,7 @@ code {
|
|
|
2167
2214
|
}
|
|
2168
2215
|
|
|
2169
2216
|
.switch-track span {
|
|
2170
|
-
background:
|
|
2217
|
+
background: #ef4444;
|
|
2171
2218
|
border-radius: 50%;
|
|
2172
2219
|
height: 14px;
|
|
2173
2220
|
transform: translateX(0);
|
|
@@ -2176,10 +2223,15 @@ code {
|
|
|
2176
2223
|
}
|
|
2177
2224
|
|
|
2178
2225
|
.switch-track.checked span {
|
|
2179
|
-
background:
|
|
2226
|
+
background: #22c55e;
|
|
2180
2227
|
transform: translateX(14px);
|
|
2181
2228
|
}
|
|
2182
2229
|
|
|
2230
|
+
.switch-track.checked {
|
|
2231
|
+
background: color-mix(in srgb, #22c55e 18%, var(--surface-muted));
|
|
2232
|
+
border-color: color-mix(in srgb, #22c55e 48%, var(--border));
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2183
2235
|
.app-meta small {
|
|
2184
2236
|
color: var(--text-muted);
|
|
2185
2237
|
font-size: 0.72rem;
|
|
@@ -2195,6 +2247,17 @@ code {
|
|
|
2195
2247
|
word-break: break-word;
|
|
2196
2248
|
}
|
|
2197
2249
|
|
|
2250
|
+
.app-log-link {
|
|
2251
|
+
color: var(--text-primary);
|
|
2252
|
+
text-decoration: none;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
.app-log-link:hover,
|
|
2256
|
+
.app-log-link:focus-visible {
|
|
2257
|
+
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
|
|
2258
|
+
outline: none;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2198
2261
|
.app-action-button {
|
|
2199
2262
|
align-items: center;
|
|
2200
2263
|
background: color-mix(in srgb, var(--accent) 16%, var(--surface-elevated));
|
|
@@ -2278,6 +2341,45 @@ code {
|
|
|
2278
2341
|
word-break: break-word;
|
|
2279
2342
|
}
|
|
2280
2343
|
|
|
2344
|
+
.modal-backdrop {
|
|
2345
|
+
align-items: center;
|
|
2346
|
+
background: rgb(0 0 0 / 62%);
|
|
2347
|
+
display: flex;
|
|
2348
|
+
inset: 0;
|
|
2349
|
+
justify-content: center;
|
|
2350
|
+
padding: 18px;
|
|
2351
|
+
position: fixed;
|
|
2352
|
+
z-index: 100;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
.confirm-modal {
|
|
2356
|
+
background: var(--surface-elevated);
|
|
2357
|
+
border: 1px solid var(--border-hover);
|
|
2358
|
+
border-radius: 18px;
|
|
2359
|
+
box-shadow: 0 24px 90px rgb(0 0 0 / 48%);
|
|
2360
|
+
display: grid;
|
|
2361
|
+
gap: 12px;
|
|
2362
|
+
max-width: 520px;
|
|
2363
|
+
padding: 22px;
|
|
2364
|
+
width: min(100%, 520px);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
.confirm-modal h3,
|
|
2368
|
+
.confirm-modal p {
|
|
2369
|
+
margin: 0;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
.confirm-modal code {
|
|
2373
|
+
overflow-wrap: anywhere;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
.confirm-modal-actions {
|
|
2377
|
+
display: flex;
|
|
2378
|
+
flex-wrap: wrap;
|
|
2379
|
+
gap: 10px;
|
|
2380
|
+
justify-content: flex-end;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2281
2383
|
[data-tooltip] {
|
|
2282
2384
|
position: relative;
|
|
2283
2385
|
}
|
|
@@ -2292,7 +2394,7 @@ code {
|
|
|
2292
2394
|
content: attr(data-tooltip);
|
|
2293
2395
|
font-size: 0.72rem;
|
|
2294
2396
|
font-weight: 700;
|
|
2295
|
-
left: 50
|
|
2397
|
+
left: clamp(10px, 50%, calc(100vw - 330px));
|
|
2296
2398
|
line-height: 1.35;
|
|
2297
2399
|
max-width: min(320px, 72vw);
|
|
2298
2400
|
opacity: 0;
|
|
@@ -2303,7 +2405,7 @@ code {
|
|
|
2303
2405
|
transform: translate(-50%, 4px);
|
|
2304
2406
|
transition: opacity 140ms ease, transform 140ms ease;
|
|
2305
2407
|
width: max-content;
|
|
2306
|
-
z-index:
|
|
2408
|
+
z-index: 400;
|
|
2307
2409
|
}
|
|
2308
2410
|
|
|
2309
2411
|
[data-tooltip]:hover::after,
|
|
@@ -2382,10 +2484,82 @@ code {
|
|
|
2382
2484
|
background: var(--surface-muted);
|
|
2383
2485
|
border: 1px solid var(--border);
|
|
2384
2486
|
border-radius: 999px;
|
|
2385
|
-
|
|
2487
|
+
align-items: center;
|
|
2488
|
+
display: inline-flex;
|
|
2386
2489
|
gap: 4px;
|
|
2387
2490
|
padding: 5px;
|
|
2388
|
-
width:
|
|
2491
|
+
width: max-content;
|
|
2492
|
+
max-width: 100%;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
.app-subnav {
|
|
2496
|
+
justify-self: start;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
.recommended-app-grid {
|
|
2500
|
+
display: grid;
|
|
2501
|
+
gap: 12px;
|
|
2502
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.recommended-app-card {
|
|
2506
|
+
background:
|
|
2507
|
+
linear-gradient(135deg, color-mix(in srgb, var(--accent) 6%, transparent), transparent 40%),
|
|
2508
|
+
var(--surface-muted);
|
|
2509
|
+
border: 1px solid var(--border);
|
|
2510
|
+
border-radius: 18px;
|
|
2511
|
+
display: grid;
|
|
2512
|
+
gap: 12px;
|
|
2513
|
+
padding: 16px;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
.recommended-app-card strong {
|
|
2517
|
+
font-size: 1.05rem;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
.recommended-app-card p {
|
|
2521
|
+
color: var(--text-secondary);
|
|
2522
|
+
margin: 4px 0 0;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
.os-icon-list {
|
|
2526
|
+
display: flex;
|
|
2527
|
+
flex-wrap: wrap;
|
|
2528
|
+
gap: 6px;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
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 {
|
|
2539
|
+
align-items: center;
|
|
2540
|
+
background: transparent;
|
|
2541
|
+
border: 0;
|
|
2542
|
+
border-radius: 0;
|
|
2543
|
+
color: var(--text-secondary);
|
|
2544
|
+
display: inline-flex;
|
|
2545
|
+
font-size: 1.12rem;
|
|
2546
|
+
font-weight: 900;
|
|
2547
|
+
justify-content: center;
|
|
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;
|
|
2389
2563
|
}
|
|
2390
2564
|
|
|
2391
2565
|
.pill-tabs button {
|
|
@@ -2393,20 +2567,30 @@ code {
|
|
|
2393
2567
|
border: 0;
|
|
2394
2568
|
border-radius: 999px;
|
|
2395
2569
|
color: var(--text-secondary);
|
|
2396
|
-
flex:
|
|
2570
|
+
flex: 0 0 auto;
|
|
2397
2571
|
font-weight: 800;
|
|
2398
2572
|
min-height: 38px;
|
|
2399
2573
|
padding: 9px 14px;
|
|
2400
2574
|
}
|
|
2401
2575
|
|
|
2402
|
-
.pill-tabs button.active
|
|
2576
|
+
.pill-tabs button.active {
|
|
2577
|
+
background: var(--accent-aaa-bg);
|
|
2578
|
+
color: #ffffff;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2403
2581
|
.pill-tabs button:hover,
|
|
2404
2582
|
.pill-tabs button:focus-visible {
|
|
2405
|
-
background: var(--surface-elevated);
|
|
2406
|
-
color: var(--
|
|
2583
|
+
background: color-mix(in srgb, var(--accent) 16%, var(--surface-elevated));
|
|
2584
|
+
color: var(--accent);
|
|
2407
2585
|
outline: none;
|
|
2408
2586
|
}
|
|
2409
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
|
+
|
|
2410
2594
|
.count-pill,
|
|
2411
2595
|
.repo-summary-strip span {
|
|
2412
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)
|