aidevops 3.29.16 → 3.29.18

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 CHANGED
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.29.16-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.18-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.29.16
1
+ 3.29.18
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.29.16
8
+ # Version: 3.29.18
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.29.16",
3
+ "version": "3.29.18",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -148,7 +148,7 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
148
148
 
149
149
  const child = spawn(command[0], command.slice(1), {
150
150
  cwd: process.cwd(),
151
- env: { ...process.env, AIDEVOPS_NON_INTERACTIVE: "true" },
151
+ env: { ...process.env, AIDEVOPS_NON_INTERACTIVE: "true", CLICOLOR_FORCE: "1", FORCE_COLOR: "1", TERM: process.env.TERM ?? "xterm-256color" },
152
152
  stdio: ["ignore", "pipe", "pipe"],
153
153
  });
154
154
  if (child.stdout !== null) {
@@ -119,7 +119,7 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
119
119
  category: "automation",
120
120
  binary: null,
121
121
  version_args: [],
122
- install_path_refs: ["~/Library/LaunchAgents/sh.aidevops.pulse.plist", "~/.aidevops/.agent-workspace/pulse"],
122
+ install_path_refs: ["~/Library/LaunchAgents/sh.aidevops.pulse.plist", "~/Library/LaunchAgents/com.aidevops.aidevops-supervisor-pulse.plist", "~/Library/LaunchAgents/sh.aidevops.supervisor-pulse.plist", "~/.aidevops/.agent-workspace/pulse"],
123
123
  origin_website_url: "https://aidevops.sh",
124
124
  origin_repo_url: "https://github.com/marcusquinn/aidevops.git",
125
125
  aidevops_install: true,
@@ -143,7 +143,7 @@ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
143
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: "" }),
144
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 }),
145
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 }),
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: "" }),
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", "/Applications/Ollama.app", "/opt/homebrew/bin/ollama", "/usr/local/bin/ollama"], origin_website_url: "https://ollama.com", origin_repo_url: "" }),
147
147
  ];
148
148
 
149
149
  export function readManagedApps(latestVersion: string, installedVersion: string): GuiManagedAppSummary[] {
@@ -200,7 +200,7 @@ function managedAppSummary(definition: ManagedAppDefinition, latestVersion: stri
200
200
  aidevops_update: definition.aidevops_update,
201
201
  installed_version: installedVersionText,
202
202
  latest_version: latestVersionText,
203
- install_path_ref: binaryPath === null ? installPathRef : collapseHome(binaryPath),
203
+ install_path_ref: collapseHome(binaryPath ?? expandHome(installPathRef)),
204
204
  status: binaryPath !== null || definition.install_path_refs.some((pathRef) => existsSync(expandHome(pathRef))) ? "found" : "missing",
205
205
  actions: (["install", "update", "reinstall", "remove"] as const).map((action) => managedAppAction(definition.action_commands, action)),
206
206
  };
@@ -396,6 +396,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
396
396
  }
397
397
  }
398
398
 
399
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
400
+ guard let url = navigationAction.request.url else {
401
+ decisionHandler(.allow)
402
+ return
403
+ }
404
+
405
+ if isLocalAppURL(url) {
406
+ decisionHandler(.allow)
407
+ return
408
+ }
409
+
410
+ if url.scheme == "http" || url.scheme == "https" {
411
+ NSWorkspace.shared.open(url)
412
+ decisionHandler(.cancel)
413
+ return
414
+ }
415
+
416
+ decisionHandler(.allow)
417
+ }
418
+
419
+ private func isLocalAppURL(_ url: URL) -> Bool {
420
+ let host = url.host ?? ""
421
+ return (host == "127.0.0.1" || host == "localhost") && url.port == Int(webPort)
422
+ }
423
+
399
424
  private func configureMenu() {
400
425
  let appName = "aidevops"
401
426
  let mainMenu = NSMenu(title: "Main Menu")
@@ -1,8 +1,8 @@
1
1
  import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
2
- import { type Dispatch, type MouseEvent as ReactMouseEvent, type ReactElement, type SetStateAction, useEffect, useRef, useState } from "react";
2
+ import { type Dispatch, type MouseEvent as ReactMouseEvent, type ReactElement, type ReactNode, type SetStateAction, useEffect, useRef, useState } from "react";
3
3
  import type { IconType } from "react-icons";
4
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";
5
+ import { FiChevronDown, FiCode, FiDownload, FiExternalLink, FiGlobe, FiMonitor, FiRefreshCw, FiRepeat, FiTerminal, FiTrash2, FiX } from "react-icons/fi";
6
6
  import { IoLogoAndroid } from "react-icons/io";
7
7
  import { SiIos } from "react-icons/si";
8
8
  import type { InventoryColumn } from "./app-model";
@@ -23,6 +23,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
23
23
  const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
24
24
  const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
25
25
  const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
26
+ const [dismissedJobIds, setDismissedJobIds] = useState<Set<string>>(() => new Set());
26
27
  const [policyJobs, setPolicyJobs] = useState<Record<string, GuiAppActionJobSummary[]>>({});
27
28
  const [policyToggles, setPolicyToggles] = useState<ManagedPolicyToggleState>(() => readManagedPolicyToggles());
28
29
  const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
@@ -36,17 +37,24 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
36
37
  setExpandedAppId(visibleManagedApps[0]?.id ?? null);
37
38
  }, [managedCategory, status.managed_apps]);
38
39
 
40
+ const runningJobIdsKey = Object.values(jobs).filter((job) => job.status === "running").map((job) => job.id).sort().join("|");
41
+
39
42
  useEffect(() => {
40
- if (selectedJob === null || selectedJob.status !== "running") {
43
+ const runningJobIds = runningJobIdsKey.split("|").filter(Boolean);
44
+ if (runningJobIds.length === 0) {
41
45
  return undefined;
42
46
  }
43
47
 
44
- const timer = window.setInterval(() => {
45
- void refreshJob(selectedJob.id, setJobs);
46
- }, 1_500);
48
+ const refreshRunningJobs = () => {
49
+ for (const jobId of runningJobIds) {
50
+ void refreshJob(jobId, setJobs);
51
+ }
52
+ };
53
+ refreshRunningJobs();
54
+ const timer = window.setInterval(refreshRunningJobs, 1_500);
47
55
 
48
56
  return () => window.clearInterval(timer);
49
- }, [selectedJob]);
57
+ }, [runningJobIdsKey]);
50
58
 
51
59
  return (
52
60
  <section className="apps-surface" aria-label={text.apps}>
@@ -65,8 +73,15 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
65
73
  expanded={expandedAppId === app.id}
66
74
  job={jobForApp(app.id, jobs, selectedJob)}
67
75
  key={app.id}
76
+ dismissedJobIds={dismissedJobIds}
68
77
  policyJobs={policyJobs[app.id] ?? []}
78
+ onDismissJob={(jobId) => setDismissedJobIds((current) => new Set([...current, jobId]))}
69
79
  onJob={(job) => {
80
+ setDismissedJobIds((current) => {
81
+ const next = new Set(current);
82
+ next.delete(job.id);
83
+ return next;
84
+ });
70
85
  setJobs((current) => ({ ...current, [job.id]: job }));
71
86
  setSelectedJobId(job.id);
72
87
  setExpandedAppId(app.id);
@@ -92,8 +107,10 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
92
107
  );
93
108
  }
94
109
 
95
- 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 {
110
+ function ManagedAppPanel({ app, dismissedJobIds, expanded, job, onDismissJob, onJob, onPolicyToggle, onToggle, policyJobs }: { app: GuiManagedAppSummary; dismissedJobIds: Set<string>; expanded: boolean; job: GuiAppActionJobSummary | null; onDismissJob: (jobId: string) => void; onJob: (job: GuiAppActionJobSummary) => void; onPolicyToggle: (policy: ManagedPolicyId, value: boolean) => void; onToggle: () => void; policyJobs: GuiAppActionJobSummary[] }): ReactElement {
96
111
  const lockedPolicy = isEssentialManagedApp(app);
112
+ const visibleJob = job === null || dismissedJobIds.has(job.id) ? null : job;
113
+ const visiblePolicyJobs = policyJobs.filter((policyJob) => !dismissedJobIds.has(policyJob.id));
97
114
 
98
115
  return (
99
116
  <article className={expanded ? "managed-app-card expanded" : "managed-app-card"}>
@@ -122,12 +139,12 @@ function ManagedAppPanel({ app, expanded, job, onJob, onPolicyToggle, onToggle,
122
139
  <div className="managed-app-details">
123
140
  <ToggleSwitch checked={app.aidevops_install} disabled={lockedPolicy} label="setup installs" onChange={(value) => onPolicyToggle("setup", value)} />
124
141
  <ToggleSwitch checked={app.aidevops_update} disabled={lockedPolicy} label="update maintains" onChange={(value) => onPolicyToggle("update", value)} />
125
- {job ? <AppLogLink job={job} /> : null}
142
+ {visibleJob ? <AppLogLink job={visibleJob} /> : null}
126
143
  </div>
127
144
  <AppMeta className="managed-app-path" label={text.path} value={app.install_path_ref} />
128
- {job ? <AppActionTerminal job={job} /> : null}
129
- {policyJobs.map((policyJob) => <AppActionTerminal job={policyJob} key={policyJob.id} />)}
130
- {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}
145
+ {visibleJob ? <AppActionTerminal job={visibleJob} onDismiss={onDismissJob} /> : null}
146
+ {visiblePolicyJobs.map((policyJob) => <AppActionTerminal job={policyJob} key={policyJob.id} onDismiss={onDismissJob} />)}
147
+ {visibleJob === null && visiblePolicyJobs.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}
131
148
  </div> : null}
132
149
  </article>
133
150
  );
@@ -194,7 +211,7 @@ function ConfirmActionModal({ action, app, close, commandPreview, confirm }: { a
194
211
  );
195
212
  }
196
213
 
197
- function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactElement {
214
+ function AppActionTerminal({ job, onDismiss }: { job: GuiAppActionJobSummary; onDismiss: (jobId: string) => void }): ReactElement {
198
215
  const outputRef = useRef<HTMLPreElement | null>(null);
199
216
 
200
217
  useEffect(() => {
@@ -205,12 +222,71 @@ function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactEleme
205
222
 
206
223
  return (
207
224
  <section className="app-terminal" id={`app-job-${job.id}`} aria-label="App action terminal output">
208
- <header><strong>{job.app_id} {job.action}</strong><span>{job.status}{job.exit_code === null ? "" : ` (${job.exit_code})`}</span></header>
209
- <pre ref={outputRef}>{job.output.join("\n")}</pre>
225
+ <header><strong>{job.app_id} {job.action}</strong><span>{terminalStatusLabel(job)}</span><button aria-label={`Dismiss ${job.app_id} ${job.action} terminal output`} className="terminal-close-button" onClick={() => onDismiss(job.id)} title="Dismiss terminal output" type="button"><FiX aria-hidden="true" /></button></header>
226
+ <pre ref={outputRef}>{renderTerminalOutput(job.output)}</pre>
210
227
  </section>
211
228
  );
212
229
  }
213
230
 
231
+ function terminalStatusLabel(job: GuiAppActionJobSummary): string {
232
+ if (job.status === "running") {
233
+ return "running";
234
+ }
235
+ if (job.exit_code === null) {
236
+ return job.status;
237
+ }
238
+ return `${job.status} · exit ${job.exit_code}`;
239
+ }
240
+
241
+ function renderTerminalOutput(lines: string[]): ReactNode[] {
242
+ return lines.flatMap((line, index) => [
243
+ ...renderAnsiLine(line, `line-${index}`),
244
+ index === lines.length - 1 ? null : "\n",
245
+ ]).filter((node): node is ReactNode => node !== null);
246
+ }
247
+
248
+ function renderAnsiLine(line: string, keyPrefix: string): ReactNode[] {
249
+ const nodes: ReactNode[] = [];
250
+ const ansiPattern = new RegExp("\\x1b\\[([0-9;]*)m", "g");
251
+ let activeClass = "";
252
+ let lastIndex = 0;
253
+ let match = ansiPattern.exec(line);
254
+
255
+ while (match !== null) {
256
+ if (match.index > lastIndex) {
257
+ nodes.push(terminalSpan(line.slice(lastIndex, match.index), activeClass, `${keyPrefix}-${nodes.length}`));
258
+ }
259
+ activeClass = ansiClassForCodes(match[1]);
260
+ lastIndex = ansiPattern.lastIndex;
261
+ match = ansiPattern.exec(line);
262
+ }
263
+
264
+ if (lastIndex < line.length) {
265
+ nodes.push(terminalSpan(line.slice(lastIndex), activeClass, `${keyPrefix}-${nodes.length}`));
266
+ }
267
+
268
+ if (nodes.length === 0) {
269
+ nodes.push(terminalSpan(line, activeClass, `${keyPrefix}-0`));
270
+ }
271
+
272
+ return nodes;
273
+ }
274
+
275
+ function terminalSpan(textValue: string, className: string, key: string): ReactNode {
276
+ const promptClass = textValue.startsWith("$ ") ? "ansi-prompt" : "";
277
+ const combinedClass = [className, promptClass].filter(Boolean).join(" ");
278
+ return combinedClass.length > 0 ? <span className={combinedClass} key={key}>{textValue}</span> : textValue;
279
+ }
280
+
281
+ function ansiClassForCodes(rawCodes: string): string {
282
+ const codes = rawCodes.length === 0 ? [0] : rawCodes.split(";").map((code) => Number.parseInt(code, 10));
283
+ const colorCode = [...codes].reverse().find((code) => (code >= 30 && code <= 37) || (code >= 90 && code <= 97));
284
+ if (colorCode === undefined) {
285
+ return "";
286
+ }
287
+ return `ansi-fg-${colorCode}`;
288
+ }
289
+
214
290
  async function refreshJob(jobId: string, setJobs: Dispatch<SetStateAction<Record<string, GuiAppActionJobSummary>>>): Promise<void> {
215
291
  const response = await fetch(`/api/apps/jobs/${encodeURIComponent(jobId)}`);
216
292
  if (!response.ok) {
@@ -321,16 +397,16 @@ const recommendedOsTabs: TabOption<RecommendedOsId>[] = [
321
397
  ];
322
398
 
323
399
  const recommendedApps = ([
324
- { name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"], platforms: [] },
325
- { 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"] },
326
- { 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: [] },
400
+ { name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows"], platforms: [] },
401
+ { 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", iosUrl: "https://itunes.apple.com/app/bitwarden-free-password-manager/id1137397744?mt=8", androidUrl: "https://play.google.com/store/apps/details?id=com.x8bit.bitwarden", fdroidUrl: "https://mobileapp.bitwarden.com/fdroid/repo", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "cli", "api"] },
402
+ { name: "Brave Browser", description: "Privacy-focused web browser.", websiteUrl: "https://brave.com/", alternativeToUrl: "https://alternativeto.net/software/brave/", iosUrl: "https://apps.apple.com/app/brave-private-web-browser-vpn/id1052879175?mt=8", androidUrl: "https://play.google.com/store/apps/details?id=com.brave.browser", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
327
403
  { name: "Cloudron", description: "Self-hosted app platform.", websiteUrl: "https://www.cloudron.io/", alternativeToUrl: "https://alternativeto.net/software/cloudron/", os: ["linux"], platforms: ["webapp", "cli", "api"] },
328
404
  { 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"] },
329
405
  { name: "Cometly", description: "Marketing attribution analytics.", websiteUrl: "https://www.cometly.com/", os: [], platforms: ["saas", "api"] },
330
406
  { 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: [] },
331
407
  { name: "DocuSeal", description: "Open source document signing.", websiteUrl: "https://www.docuseal.com/", alternativeToUrl: "https://alternativeto.net/software/docuseal/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
332
- { 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"] },
333
- { 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: [] },
408
+ { name: "Element", description: "Matrix messaging client.", websiteUrl: "https://element.io/", alternativeToUrl: "https://alternativeto.net/software/element-app/", iosUrl: "https://apps.apple.com/app/id1631335820", androidUrl: "https://play.google.com/store/apps/details?id=io.element.android.x", fdroidUrl: "https://f-droid.org/en/packages/io.element.android.x/", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp"] },
409
+ { name: "Enpass", description: "Offline-first password manager.", websiteUrl: "https://www.enpass.io/", alternativeToUrl: "https://alternativeto.net/software/enpass/", iosUrl: "https://apps.apple.com/app/apple-store/id455566716?pt=637991", androidUrl: "https://play.google.com/store/apps/details?id=io.enpass.app", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
334
410
  { name: "EspoCRM", description: "Open source CRM.", websiteUrl: "https://www.espocrm.com/", alternativeToUrl: "https://alternativeto.net/software/espocrm/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
335
411
  { name: "Fathom Analytics", description: "Privacy-first analytics.", websiteUrl: "https://usefathom.com/", alternativeToUrl: "https://alternativeto.net/software/fathom-analytics/", os: [], platforms: ["saas", "api"] },
336
412
  { name: "FontBase", description: "Font management and reference tool.", websiteUrl: "https://fontba.se/", alternativeToUrl: "https://alternativeto.net/software/fontbase/", os: ["macos"], platforms: [] },
@@ -341,10 +417,10 @@ const recommendedApps = ([
341
417
  { name: "LibreOffice", description: "Open source office suite.", websiteUrl: "https://www.libreoffice.org/", alternativeToUrl: "https://alternativeto.net/software/libreoffice/", os: ["macos", "linux", "windows"], platforms: [] },
342
418
  { name: "LocalWP", description: "Local WordPress development.", websiteUrl: "https://localwp.com/", alternativeToUrl: "https://alternativeto.net/software/local-by-flywheel/", os: ["macos", "linux", "windows"], platforms: [] },
343
419
  { name: "Matomo", description: "Open analytics platform.", websiteUrl: "https://matomo.org/", alternativeToUrl: "https://alternativeto.net/software/piwik/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
344
- { 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"] },
420
+ { name: "Nextcloud", description: "Open source content collaboration platform.", websiteUrl: "https://nextcloud.com/", alternativeToUrl: "https://alternativeto.net/software/nextcloud/", repoUrl: "https://github.com/nextcloud", iosUrl: "https://itunes.apple.com/us/app/nextcloud/id1125420102?mt=8", androidUrl: "https://play.google.com/store/apps/details?id=com.nextcloud.client", fdroidUrl: "https://f-droid.org/packages/com.nextcloud.client/", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "api"] },
345
421
  { 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"] },
346
422
  { 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: [] },
347
- { 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"] },
423
+ { name: "ONLYOFFICE", description: "Office and document collaboration.", websiteUrl: "https://www.onlyoffice.com/", alternativeToUrl: "https://alternativeto.net/software/onlyoffice/", repoUrl: "https://github.com/ONLYOFFICE/", iosUrl: "https://apps.apple.com/us/app/onlyoffice-documents/id944896972", androidUrl: "https://play.google.com/store/apps/details?id=com.onlyoffice.documents", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "api"] },
348
424
  { 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: [] },
349
425
  { name: "Osaurus", description: "AI app workspace.", websiteUrl: "https://osaurus.ai/", alternativeToUrl: "https://alternativeto.net/software/osaurus/", os: ["macos"], platforms: [] },
350
426
  { 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: [] },
@@ -357,13 +433,13 @@ const recommendedApps = ([
357
433
  { name: "QuickFile", description: "Accounting platform.", websiteUrl: "https://www.quickfile.co.uk/", os: [], platforms: ["saas", "api"] },
358
434
  { name: "Reframed", description: "Creative framing utility.", websiteUrl: "https://www.reframed.dev/", alternativeToUrl: "https://alternativeto.net/software/reframed/", os: ["macos"], platforms: [] },
359
435
  { name: "Rybbit", description: "Web analytics.", websiteUrl: "https://rybbit.com/", alternativeToUrl: "https://alternativeto.net/software/rybbit/", os: ["linux"], platforms: ["webapp", "saas", "api"] },
360
- { name: "SchildiChat", description: "Matrix messaging client.", websiteUrl: "https://schildi.chat/", alternativeToUrl: "https://alternativeto.net/software/schildichat/", os: ["macos", "linux", "windows", "android"], platforms: ["webapp"] },
361
- { name: "ScreenFlow", description: "Screen recording and editing.", websiteUrl: "https://www.telestream.net/screenflow/", alternativeToUrl: "https://alternativeto.net/software/screenflow/", os: ["macos", "ios"], platforms: [] },
436
+ { name: "SchildiChat", description: "Matrix messaging client.", websiteUrl: "https://schildi.chat/", alternativeToUrl: "https://alternativeto.net/software/schildichat/", androidUrl: "https://play.google.com/store/apps/details?id=chat.schildi.android", fdroidUrl: "https://schildi.chat/next/install-from-sc-fdroid", os: ["macos", "linux", "windows", "android"], platforms: ["webapp"] },
437
+ { name: "ScreenFlow", description: "Screen recording and editing.", websiteUrl: "https://www.telestream.net/screenflow/", alternativeToUrl: "https://alternativeto.net/software/screenflow/", os: ["macos"], platforms: [] },
362
438
  { name: "SEO Utils", description: "Desktop SEO tools.", websiteUrl: "https://seoutils.app/", os: ["macos", "linux", "windows"], platforms: [] },
363
439
  { name: "Shottr", description: "macOS screenshot utility.", websiteUrl: "https://shottr.cc/", alternativeToUrl: "https://alternativeto.net/software/shottr/", os: ["macos"], platforms: [] },
364
- { name: "Signal", description: "Private messenger.", websiteUrl: "https://signal.org/", alternativeToUrl: "https://alternativeto.net/software/signal-private-messenger/", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
365
- { 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"] },
366
- { 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"] },
440
+ { name: "Signal", description: "Private messenger.", websiteUrl: "https://signal.org/", alternativeToUrl: "https://alternativeto.net/software/signal-private-messenger/", iosUrl: "https://apps.apple.com/us/app/signal-private-messenger/id874139669", androidUrl: "https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms", os: ["macos", "linux", "windows", "ios", "android"], platforms: [] },
441
+ { 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", iosUrl: "https://apps.apple.com/us/app/simplex-chat/id1605771084", androidUrl: "https://play.google.com/store/apps/details?id=chat.simplex.app", fdroidUrl: "https://simplex.chat/fdroid", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["cli"] },
442
+ { name: "Telegram", description: "Cloud-based mobile and desktop messaging app.", websiteUrl: "https://telegram.org/", iosUrl: "https://apps.apple.com/us/app/telegram-messenger/id686449807", androidUrl: "https://play.google.com/store/apps/details?id=org.telegram.messenger", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "api"] },
367
443
  { 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: [] },
368
444
  { 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"] },
369
445
  { 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"] },
@@ -147,7 +147,7 @@ export const text = {
147
147
  aidevops: "aidevops",
148
148
  appShell: "App shell",
149
149
  apps: "Apps",
150
- appsIntro: "The app toolkit we use and recommend to enable all the things we can do with AI, development, and operations to create and share our work. Opinionated preferences based on open-source-first, lowest-costs, highest-quality UI & UX, teamwork, data security, privacy, and AI-usage capabilities.",
150
+ appsIntro: "These are the apps we use and recommend from our tried & tested toolkit — enabling all the things we can do with AI, development, and operations to create and share our work. Opinionated preferences based on open-source-first, lowest-costs, highest-quality UI & UX, teamwork, data security, privacy, and AI-usage capabilities.",
151
151
  appStatusPending: "adapter pending",
152
152
  brands: "Brands",
153
153
  brandsIntro: "Draft brand inventory for names and website references.",
@@ -1316,7 +1316,8 @@ code {
1316
1316
  .workspace-scroll {
1317
1317
  display: grid;
1318
1318
  gap: 16px;
1319
- overflow: auto;
1319
+ overflow-x: hidden;
1320
+ overflow-y: auto;
1320
1321
  padding: 18px;
1321
1322
  }
1322
1323
 
@@ -2053,7 +2054,8 @@ code {
2053
2054
  border: 1px solid var(--border);
2054
2055
  border-radius: 18px;
2055
2056
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
2056
- overflow: visible;
2057
+ min-width: 0;
2058
+ overflow: hidden;
2057
2059
  transition: background 140ms ease, border-color 140ms ease;
2058
2060
  }
2059
2061
 
@@ -2174,6 +2176,7 @@ code {
2174
2176
  gap: 16px;
2175
2177
  padding: 0 16px 16px;
2176
2178
  position: relative;
2179
+ min-width: 0;
2177
2180
  }
2178
2181
 
2179
2182
  .managed-app-toolbar {
@@ -2182,6 +2185,7 @@ code {
2182
2185
  gap: 16px;
2183
2186
  grid-template-columns: minmax(0, 1fr) auto;
2184
2187
  padding-top: 16px;
2188
+ min-width: 0;
2185
2189
  }
2186
2190
 
2187
2191
  .managed-app-links,
@@ -2194,6 +2198,7 @@ code {
2194
2198
 
2195
2199
  .managed-app-actions {
2196
2200
  justify-content: end;
2201
+ min-width: 0;
2197
2202
  }
2198
2203
 
2199
2204
  .managed-app-links a,
@@ -2393,6 +2398,7 @@ button.managed-toggle:focus-visible {
2393
2398
  border: 1px solid var(--border);
2394
2399
  border-radius: 16px;
2395
2400
  color: color-mix(in srgb, var(--accent) 78%, white);
2401
+ min-width: 0;
2396
2402
  overflow: hidden;
2397
2403
  }
2398
2404
 
@@ -2405,11 +2411,40 @@ button.managed-toggle:focus-visible {
2405
2411
  padding: 10px 12px;
2406
2412
  }
2407
2413
 
2414
+ .app-terminal header strong {
2415
+ min-width: 0;
2416
+ overflow: hidden;
2417
+ text-overflow: ellipsis;
2418
+ white-space: nowrap;
2419
+ }
2420
+
2408
2421
  .app-terminal header span {
2422
+ flex: 0 0 auto;
2409
2423
  color: var(--text-muted);
2410
2424
  text-align: right;
2411
2425
  }
2412
2426
 
2427
+ .terminal-close-button {
2428
+ align-items: center;
2429
+ background: transparent;
2430
+ border: 1px solid transparent;
2431
+ border-radius: 999px;
2432
+ color: var(--text-muted);
2433
+ display: inline-flex;
2434
+ flex: 0 0 auto;
2435
+ height: 28px;
2436
+ justify-content: center;
2437
+ padding: 0;
2438
+ width: 28px;
2439
+ }
2440
+
2441
+ .terminal-close-button:hover,
2442
+ .terminal-close-button:focus-visible {
2443
+ border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
2444
+ color: var(--accent);
2445
+ outline: none;
2446
+ }
2447
+
2413
2448
  .app-terminal pre {
2414
2449
  margin: 0;
2415
2450
  max-height: 240px;
@@ -2419,6 +2454,24 @@ button.managed-toggle:focus-visible {
2419
2454
  word-break: break-word;
2420
2455
  }
2421
2456
 
2457
+ .ansi-prompt { color: #7ee787; }
2458
+ .ansi-fg-30,
2459
+ .ansi-fg-90 { color: #8b949e; }
2460
+ .ansi-fg-31,
2461
+ .ansi-fg-91 { color: #ff7b72; }
2462
+ .ansi-fg-32,
2463
+ .ansi-fg-92 { color: #7ee787; }
2464
+ .ansi-fg-33,
2465
+ .ansi-fg-93 { color: #f2cc60; }
2466
+ .ansi-fg-34,
2467
+ .ansi-fg-94 { color: #79c0ff; }
2468
+ .ansi-fg-35,
2469
+ .ansi-fg-95 { color: #d2a8ff; }
2470
+ .ansi-fg-36,
2471
+ .ansi-fg-96 { color: #76e3ea; }
2472
+ .ansi-fg-37,
2473
+ .ansi-fg-97 { color: #f0f6fc; }
2474
+
2422
2475
  .modal-backdrop {
2423
2476
  align-items: center;
2424
2477
  background: rgb(0 0 0 / 62%);
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.16
15
+ # Version: 3.29.18
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)