aidevops 3.28.0 → 3.29.0

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.28.0-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.0-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.28.0
1
+ 3.29.0
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.28.0
8
+ # Version: 3.29.0
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.28.0",
3
+ "version": "3.29.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,8 +1,37 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { Hono } from "hono";
2
- import { BANNED_ROUTE_PATTERNS, FILE_EXPLORER_ROUTE_MANIFEST, STATUS_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
3
+ import { APP_ACTION_ROUTE_MANIFEST, APP_ACTION_STATUS_ROUTE_MANIFEST, BANNED_ROUTE_PATTERNS, createEnvelope, FILE_EXPLORER_ROUTE_MANIFEST, type GuiAppActionId, type GuiAppActionJobSummary, STATUS_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
3
4
  import { readFileExplorer } from "./file-adapter";
4
5
  import { readStatus, readVaultStatus } from "./status-adapter";
5
6
 
7
+ const appActionCommands: Record<string, Partial<Record<GuiAppActionId, string[]>>> = {
8
+ aidevops: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update"], reinstall: ["./setup.sh", "--non-interactive"] },
9
+ agents: { install: ["aidevops", "setup", "--scope", "agents"], update: ["aidevops", "setup", "--scope", "agents"], reinstall: ["aidevops", "setup", "--scope", "agents"] },
10
+ "gui-desktop": { install: ["aidevops", "setup", "--scope", "gui-desktop"], update: ["aidevops", "setup", "--scope", "gui-desktop"], reinstall: ["aidevops", "setup", "--scope", "gui-desktop"] },
11
+ hooks: { install: ["aidevops", "setup", "--scope", "hooks"], update: ["aidevops", "setup", "--scope", "hooks"], reinstall: ["aidevops", "setup", "--scope", "hooks"] },
12
+ opencode: { install: ["aidevops", "setup", "--scope", "opencode"], update: ["aidevops", "setup", "--scope", "opencode"], reinstall: ["aidevops", "setup", "--scope", "opencode"] },
13
+ pulse: { install: ["aidevops", "setup", "--scope", "pulse"], update: ["aidevops", "setup", "--scope", "pulse"], reinstall: ["aidevops", "setup", "--scope", "pulse"] },
14
+ tabby: { install: ["aidevops", "setup", "--scope", "tabby"], update: ["aidevops", "setup", "--scope", "tabby"], reinstall: ["aidevops", "setup", "--scope", "tabby"] },
15
+ bun: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
16
+ cursor: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
17
+ fd: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
18
+ gh: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
19
+ glab: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
20
+ homebrew: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
21
+ node: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
22
+ ollama: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
23
+ orbstack: { install: ["./setup.sh", "--non-interactive"] },
24
+ qlty: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
25
+ ripgrep: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
26
+ "ripgrep-all": { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
27
+ rtk: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
28
+ shellcheck: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
29
+ shfmt: { install: ["./setup.sh", "--non-interactive"], update: ["aidevops", "update-tools", "--update"] },
30
+ zed: { install: ["./setup.sh", "--non-interactive"] },
31
+ };
32
+
33
+ const appActionJobs = new Map<string, GuiAppActionJobSummary>();
34
+
6
35
  export function createGuiApiApp() {
7
36
  const app = new Hono();
8
37
 
@@ -14,6 +43,48 @@ export function createGuiApiApp() {
14
43
  return context.json(readVaultStatus());
15
44
  });
16
45
 
46
+ app.post(APP_ACTION_ROUTE_MANIFEST.route, (context) => {
47
+ const appId = context.req.param("appId");
48
+ const action = context.req.param("action") as GuiAppActionId;
49
+ const command = appActionCommands[appId]?.[action];
50
+
51
+ if (command === undefined) {
52
+ return context.json(createEnvelope({
53
+ operation_id: APP_ACTION_ROUTE_MANIFEST.operation_id,
54
+ source: { surface: "apps", authority: "allowlisted local command runner", path_refs: [] },
55
+ data: rejectedJob(appId, action, "No allowlisted command for this app action."),
56
+ errors: ["action_not_allowlisted"],
57
+ }), 400);
58
+ }
59
+
60
+ const job = startAppActionJob(appId, action, command);
61
+ return context.json(createEnvelope({
62
+ operation_id: APP_ACTION_ROUTE_MANIFEST.operation_id,
63
+ source: { surface: "apps", authority: "allowlisted local command runner", path_refs: ["setup.sh", "aidevops.sh"] },
64
+ data: job,
65
+ warnings: ["Command runs locally in the background. Output is retained in memory for this GUI API process only."],
66
+ }), 202);
67
+ });
68
+
69
+ app.get(APP_ACTION_STATUS_ROUTE_MANIFEST.route, (context) => {
70
+ const jobId = context.req.param("jobId");
71
+ const job = appActionJobs.get(jobId);
72
+ if (job === undefined) {
73
+ return context.json(createEnvelope({
74
+ operation_id: APP_ACTION_STATUS_ROUTE_MANIFEST.operation_id,
75
+ source: { surface: "apps", authority: "background job store", path_refs: [] },
76
+ data: rejectedJob("unknown", "install", "Unknown job."),
77
+ errors: ["unknown_job"],
78
+ }), 404);
79
+ }
80
+
81
+ return context.json(createEnvelope({
82
+ operation_id: APP_ACTION_STATUS_ROUTE_MANIFEST.operation_id,
83
+ source: { surface: "apps", authority: "background job store", path_refs: [] },
84
+ data: job,
85
+ }));
86
+ });
87
+
17
88
  app.get(FILE_EXPLORER_ROUTE_MANIFEST.route, (context) => {
18
89
  const root = context.req.param("root");
19
90
  const path = context.req.query("path") ?? "";
@@ -50,4 +121,57 @@ export function createGuiApiApp() {
50
121
  return app;
51
122
  }
52
123
 
124
+ function startAppActionJob(appId: string, action: GuiAppActionId, command: string[]): GuiAppActionJobSummary {
125
+ const now = new Date().toISOString();
126
+ const job: GuiAppActionJobSummary = {
127
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
128
+ app_id: appId,
129
+ action,
130
+ status: "running",
131
+ command_preview: command.join(" "),
132
+ started_at: now,
133
+ finished_at: null,
134
+ exit_code: null,
135
+ output: [`$ ${command.join(" ")}`],
136
+ };
137
+ appActionJobs.set(job.id, job);
138
+
139
+ const child = spawn(command[0], command.slice(1), {
140
+ cwd: process.cwd(),
141
+ env: { ...process.env, AIDEVOPS_NON_INTERACTIVE: "true" },
142
+ stdio: ["ignore", "pipe", "pipe"],
143
+ });
144
+ child.stdout.on("data", (chunk) => appendJobOutput(job, chunk.toString()));
145
+ child.stderr.on("data", (chunk) => appendJobOutput(job, chunk.toString()));
146
+ child.on("error", (error) => {
147
+ appendJobOutput(job, error.message);
148
+ job.status = "failed";
149
+ job.finished_at = new Date().toISOString();
150
+ job.exit_code = 127;
151
+ });
152
+ child.on("close", (code) => {
153
+ job.status = code === 0 ? "completed" : "failed";
154
+ job.finished_at = new Date().toISOString();
155
+ job.exit_code = code;
156
+ });
157
+
158
+ return job;
159
+ }
160
+
161
+ function appendJobOutput(job: GuiAppActionJobSummary, output: string): void {
162
+ job.output.push(...output.split(/\r?\n/).filter((line) => line.length > 0).map(redactOutputLine));
163
+ if (job.output.length > 400) {
164
+ job.output.splice(1, job.output.length - 400);
165
+ }
166
+ }
167
+
168
+ function redactOutputLine(line: string): string {
169
+ return line.replace(/(token|secret|password|authorization)=\S+/gi, "$1=[redacted]");
170
+ }
171
+
172
+ function rejectedJob(appId: string, action: GuiAppActionId, message: string): GuiAppActionJobSummary {
173
+ const now = new Date().toISOString();
174
+ return { id: "rejected", app_id: appId, action, status: "rejected", command_preview: "not run", started_at: now, finished_at: now, exit_code: null, output: [message] };
175
+ }
176
+
53
177
  export const app = createGuiApiApp();
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { existsSync, readFileSync, realpathSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
 
@@ -106,3 +107,37 @@ export function readOptionalText(pathName: string): string | null {
106
107
 
107
108
  return readFileSync(pathName, "utf8").trim();
108
109
  }
110
+
111
+ export function firstExistingPathRef(pathRefs: string[]): string | null {
112
+ return pathRefs.find((pathRef) => existsSync(expandHome(pathRef))) ?? null;
113
+ }
114
+
115
+ export function resolveBinary(binary: string): string | null {
116
+ try {
117
+ const output = execFileSync("which", [binary], {
118
+ encoding: "utf8",
119
+ stdio: ["ignore", "pipe", "ignore"],
120
+ timeout: 200,
121
+ }).trim();
122
+ return output.split("\n").find((line) => line.length > 0) ?? null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ export function readBinaryVersion(binaryPath: string, args: string[]): string {
129
+ if (args.length === 0) {
130
+ return "unknown";
131
+ }
132
+
133
+ try {
134
+ const output = execFileSync(binaryPath, args, {
135
+ encoding: "utf8",
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ timeout: 500,
138
+ }).trim();
139
+ return output.split("\n").find((line) => line.length > 0) ?? "unknown";
140
+ } catch {
141
+ return "unknown";
142
+ }
143
+ }
@@ -1,19 +1,18 @@
1
+ import { Database } from "bun:sqlite";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { hostname, networkInterfaces, userInfo } from "node:os";
4
5
  import { join } from "node:path";
5
- import { Database } from "bun:sqlite";
6
6
  import {
7
- type GuiLocalRepoSetupSummary,
8
- type GuiOpenCodeSessionRegistrySummary,
9
- type GuiOpenCodeSessionSummary,
10
7
  assertNoSecretSentinels,
11
8
  createEnvelope,
12
- VAULT_STATUS_ROUTE_MANIFEST,
13
9
  type GuiAiAppSummary,
14
10
  type GuiAiProviderId,
11
+ type GuiLocalRepoSetupSummary,
15
12
  type GuiOAuthPoolSummary,
16
13
  type GuiOAuthProviderSummary,
14
+ type GuiOpenCodeSessionRegistrySummary,
15
+ type GuiOpenCodeSessionSummary,
17
16
  type GuiRepoRegistrySummary,
18
17
  type GuiRepoSummary,
19
18
  type GuiResponseEnvelope,
@@ -22,20 +21,25 @@ import {
22
21
  type GuiStatusData,
23
22
  type GuiVaultStatusData,
24
23
  statusFixture,
24
+ VAULT_STATUS_ROUTE_MANIFEST,
25
25
  } from "../../gui-shared/src";
26
26
  import {
27
27
  collapseHome,
28
28
  cooldownReady,
29
29
  expandHome,
30
+ firstExistingPathRef,
30
31
  formatEpochField,
31
32
  formatNullableEpochField,
32
33
  isRecord,
33
34
  numberField,
35
+ readBinaryVersion,
34
36
  readJsonObject,
35
37
  readOptionalText,
38
+ resolveBinary,
36
39
  stringField,
37
40
  } from "./status-adapter-utils";
38
41
  import { readLocalReposSetupSummary } from "./status-local-repos";
42
+ import { readManagedApps } from "./status-managed-apps";
39
43
  import { readVaultSummary } from "./status-vault";
40
44
 
41
45
  export { readVaultSummary } from "./status-vault";
@@ -69,6 +73,7 @@ export function readStatus(
69
73
  const restartRequired = installedVersion !== aidevopsVersion;
70
74
  const setupTargets = readSetupTargets(aidevopsVersion || "unknown", installedVersion || "unknown");
71
75
  const aiApps = readAiApps(aidevopsVersion || "unknown", installedVersion || "unknown");
76
+ const managedApps = readManagedApps(aidevopsVersion || "unknown", installedVersion || "unknown");
72
77
  const localRepos = readLocalReposSetupSummary(reposPath);
73
78
  const opencodeSessions = readOpenCodeSessions(opencodeDbPath, opencodeDbPathRef, localRepos.repos);
74
79
  const vault = readVaultSummary(repoRoot);
@@ -82,6 +87,7 @@ export function readStatus(
82
87
  "VERSION",
83
88
  ...setupTargets.map((target) => target.path_ref),
84
89
  ...aiApps.flatMap((app) => [app.app_path_ref, app.binary_path_ref, app.config_path_ref, app.aidevops_target_path_ref]),
90
+ ...managedApps.map((app) => app.install_path_ref),
85
91
  ].filter(isSourcePathRef);
86
92
 
87
93
  const data: GuiStatusData = {
@@ -128,6 +134,7 @@ export function readStatus(
128
134
  oauth_pool: readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef),
129
135
  setup_targets: setupTargets,
130
136
  ai_apps: aiApps,
137
+ managed_apps: managedApps,
131
138
  vault,
132
139
  };
133
140
 
@@ -412,23 +419,6 @@ function aiAppSummary(definition: AiAppDefinition, latestVersion: string, instal
412
419
  };
413
420
  }
414
421
 
415
- function firstExistingPathRef(pathRefs: string[]): string | null {
416
- return pathRefs.find((pathRef) => existsSync(expandHome(pathRef))) ?? null;
417
- }
418
-
419
- function resolveBinary(binary: string): string | null {
420
- try {
421
- const output = execFileSync("/usr/bin/which", [binary], {
422
- encoding: "utf8",
423
- stdio: ["ignore", "pipe", "ignore"],
424
- timeout: 1_000,
425
- }).trim();
426
- return output.split("\n").find((line) => line.length > 0) ?? null;
427
- } catch {
428
- return null;
429
- }
430
- }
431
-
432
422
  interface OpenCodeSessionRow {
433
423
  id: string;
434
424
  directory: string;
@@ -506,23 +496,6 @@ function formatUnixMillis(value: number): string {
506
496
  return new Date(value).toISOString();
507
497
  }
508
498
 
509
- function readBinaryVersion(binaryPath: string, args: string[]): string {
510
- if (args.length === 0) {
511
- return "unknown";
512
- }
513
-
514
- try {
515
- const output = execFileSync(binaryPath, args, {
516
- encoding: "utf8",
517
- stdio: ["ignore", "pipe", "ignore"],
518
- timeout: 1_500,
519
- }).trim();
520
- return output.split("\n").find((line) => line.length > 0) ?? "unknown";
521
- } catch {
522
- return "unknown";
523
- }
524
- }
525
-
526
499
  function isSourcePathRef(pathRef: string): boolean {
527
500
  return pathRef === "VERSION" || pathRef.startsWith("~/") || pathRef.startsWith("/");
528
501
  }
@@ -0,0 +1,228 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { GuiAppActionId, GuiManagedAppSummary } from "../../gui-shared/src";
3
+ import { collapseHome, expandHome, firstExistingPathRef, readBinaryVersion, resolveBinary } from "./status-adapter-utils";
4
+
5
+ type ManagedAppActionCommands = Partial<Record<GuiAppActionId, string>>;
6
+
7
+ interface ManagedAppDefinition {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ category: string;
12
+ binary: string | null;
13
+ version_args: string[];
14
+ install_path_refs: string[];
15
+ origin_website_url: string;
16
+ origin_repo_url: string;
17
+ aidevops_install: boolean;
18
+ aidevops_update: boolean;
19
+ latest_version_source: "aidevops" | "binary" | "unknown";
20
+ action_commands: ManagedAppActionCommands;
21
+ }
22
+
23
+ type BinaryAppInput = Omit<ManagedAppDefinition, "action_commands" | "aidevops_install" | "aidevops_update" | "latest_version_source"> & {
24
+ binary: string;
25
+ aidevops_install?: boolean;
26
+ aidevops_update?: boolean;
27
+ };
28
+
29
+ const TOOL_ACTIONS: ManagedAppActionCommands = {
30
+ install: "./setup.sh --non-interactive",
31
+ update: "aidevops update-tools --update",
32
+ };
33
+
34
+ const MANAGED_APP_DEFINITIONS: ManagedAppDefinition[] = [
35
+ {
36
+ id: "aidevops",
37
+ name: "aidevops",
38
+ description: "Framework CLI, agents, workflows, scripts, and local GUI assets.",
39
+ category: "core",
40
+ binary: "aidevops",
41
+ version_args: ["--version"],
42
+ install_path_refs: ["~/.aidevops/agents", "/opt/homebrew/bin/aidevops", "/usr/local/bin/aidevops"],
43
+ origin_website_url: "https://aidevops.sh",
44
+ origin_repo_url: "https://github.com/marcusquinn/aidevops.git",
45
+ aidevops_install: true,
46
+ aidevops_update: true,
47
+ latest_version_source: "aidevops",
48
+ action_commands: {
49
+ install: "./setup.sh --non-interactive",
50
+ update: "aidevops update",
51
+ reinstall: "./setup.sh --non-interactive",
52
+ },
53
+ },
54
+ {
55
+ id: "agents",
56
+ name: "Deployed agents",
57
+ description: "Canonical aidevops agents, commands, workflows, scripts, and reference files.",
58
+ category: "core",
59
+ binary: null,
60
+ version_args: [],
61
+ install_path_refs: ["~/.aidevops/agents/VERSION"],
62
+ origin_website_url: "https://aidevops.sh",
63
+ origin_repo_url: "https://github.com/marcusquinn/aidevops.git",
64
+ aidevops_install: true,
65
+ aidevops_update: true,
66
+ latest_version_source: "aidevops",
67
+ action_commands: setupScopeActions("agents"),
68
+ },
69
+ {
70
+ id: "gui-desktop",
71
+ name: "aidevops.app",
72
+ description: "Native macOS desktop launcher for the local GUI.",
73
+ category: "desktop",
74
+ binary: null,
75
+ version_args: [],
76
+ install_path_refs: ["~/Applications/aidevops.app", "/Applications/aidevops.app"],
77
+ origin_website_url: "https://aidevops.sh",
78
+ origin_repo_url: "https://github.com/marcusquinn/aidevops.git",
79
+ aidevops_install: true,
80
+ aidevops_update: true,
81
+ latest_version_source: "aidevops",
82
+ action_commands: setupScopeActions("gui-desktop"),
83
+ },
84
+ {
85
+ id: "opencode",
86
+ name: "OpenCode",
87
+ description: "AI terminal runtime configured by aidevops for local sessions.",
88
+ category: "ai runtime",
89
+ binary: "opencode",
90
+ version_args: ["--version"],
91
+ install_path_refs: ["~/Applications/OpenCode AIDevOps.app", "/Applications/OpenCode.app", "~/.config/opencode/opencode.json"],
92
+ origin_website_url: "https://opencode.ai",
93
+ origin_repo_url: "",
94
+ aidevops_install: true,
95
+ aidevops_update: true,
96
+ latest_version_source: "binary",
97
+ action_commands: setupScopeActions("opencode"),
98
+ },
99
+ {
100
+ id: "hooks",
101
+ name: "Safety hooks",
102
+ description: "Git, prompt, privacy, complexity, task-id, and canonical-install guards.",
103
+ category: "safety",
104
+ binary: null,
105
+ version_args: [],
106
+ install_path_refs: ["~/.aidevops/hooks", "~/.config/aidevops"],
107
+ origin_website_url: "https://aidevops.sh",
108
+ origin_repo_url: "https://github.com/marcusquinn/aidevops.git",
109
+ aidevops_install: true,
110
+ aidevops_update: true,
111
+ latest_version_source: "aidevops",
112
+ action_commands: setupScopeActions("hooks"),
113
+ },
114
+ {
115
+ id: "pulse",
116
+ name: "Pulse scheduler",
117
+ description: "Launchd/cron supervisor for autonomous aidevops maintenance and dispatch routines.",
118
+ category: "automation",
119
+ binary: null,
120
+ version_args: [],
121
+ install_path_refs: ["~/Library/LaunchAgents/sh.aidevops.pulse.plist", "~/.aidevops/.agent-workspace/pulse"],
122
+ origin_website_url: "https://aidevops.sh",
123
+ origin_repo_url: "https://github.com/marcusquinn/aidevops.git",
124
+ aidevops_install: true,
125
+ aidevops_update: true,
126
+ latest_version_source: "aidevops",
127
+ action_commands: setupScopeActions("pulse"),
128
+ },
129
+ binaryApp({ id: "tabby", name: "Tabby terminal profiles", description: "Terminal profile sync for aidevops-managed shells.", category: "terminal", binary: "tabby", version_args: ["--version"], install_path_refs: ["~/Library/Application Support/tabby", "/Applications/Tabby.app"], origin_website_url: "https://github.com/Eugeny/tabby/releases/latest", origin_repo_url: "https://github.com/Eugeny/tabby/releases/latest" }),
130
+ binaryApp({ id: "gh", name: "GitHub CLI", description: "GitHub issues, PRs, checks, releases, and API automation.", category: "git", binary: "gh", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"], origin_website_url: "", origin_repo_url: "" }),
131
+ binaryApp({ id: "glab", name: "GitLab CLI", description: "GitLab repository, issue, merge request, and CI automation.", category: "git", binary: "glab", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/glab", "/usr/local/bin/glab", "/usr/bin/glab"], origin_website_url: "", origin_repo_url: "" }),
132
+ binaryApp({ id: "fd", name: "fd", description: "Fast file discovery for agents and developer workflows.", category: "search", binary: "fd", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/fd", "/usr/local/bin/fd", "/usr/bin/fd"], origin_website_url: "", origin_repo_url: "" }),
133
+ binaryApp({ id: "ripgrep", name: "ripgrep", description: "Fast code and text search used by agents and local diagnostics.", category: "search", binary: "rg", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/rg", "/usr/local/bin/rg", "/usr/bin/rg"], origin_website_url: "", origin_repo_url: "" }),
134
+ binaryApp({ id: "ripgrep-all", name: "ripgrep-all", description: "Document/archive-aware search companion for ripgrep.", category: "search", binary: "rga", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/rga", "/usr/local/bin/rga", "/usr/bin/rga"], origin_website_url: "", origin_repo_url: "" }),
135
+ binaryApp({ id: "shellcheck", name: "ShellCheck", description: "Shell script static analysis gate.", category: "quality", binary: "shellcheck", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/shellcheck", "/usr/local/bin/shellcheck", "/usr/bin/shellcheck"], origin_website_url: "", origin_repo_url: "" }),
136
+ binaryApp({ id: "shfmt", name: "shfmt", description: "Shell formatter used by local quality workflows.", category: "quality", binary: "shfmt", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/shfmt", "/usr/local/bin/shfmt", "/usr/bin/shfmt"], origin_website_url: "", origin_repo_url: "" }),
137
+ binaryApp({ id: "bun", name: "Bun", description: "JavaScript runtime used for the GUI and toolchain helpers.", category: "runtime", binary: "bun", version_args: ["--version"], install_path_refs: ["~/.bun/bin/bun", "/opt/homebrew/bin/bun", "/usr/local/bin/bun"], origin_website_url: "https://bun.sh/install", origin_repo_url: "" }),
138
+ binaryApp({ id: "node", name: "Node.js", description: "JavaScript runtime required by npm-hosted helpers and frontend tooling.", category: "runtime", binary: "node", version_args: ["--version"], install_path_refs: ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"], origin_website_url: "https://nodejs.org/", origin_repo_url: "" }),
139
+ 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
+ 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
+ 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: "ai runtime", 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
+ 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
+ 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
+ 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
+ ];
147
+
148
+ export function readManagedApps(latestVersion: string, installedVersion: string): GuiManagedAppSummary[] {
149
+ return MANAGED_APP_DEFINITIONS.map((definition) => managedAppSummary(definition, latestVersion, installedVersion));
150
+ }
151
+
152
+ function binaryApp(input: BinaryAppInput): ManagedAppDefinition {
153
+ const aidevopsInstall = input.aidevops_install ?? true;
154
+ const aidevopsUpdate = input.aidevops_update ?? true;
155
+
156
+ return {
157
+ id: input.id,
158
+ name: input.name,
159
+ description: input.description,
160
+ category: input.category,
161
+ binary: input.binary,
162
+ version_args: input.version_args,
163
+ install_path_refs: input.install_path_refs,
164
+ origin_website_url: input.origin_website_url,
165
+ origin_repo_url: input.origin_repo_url,
166
+ aidevops_install: aidevopsInstall,
167
+ aidevops_update: aidevopsUpdate,
168
+ latest_version_source: "binary",
169
+ action_commands: aidevopsUpdate ? TOOL_ACTIONS : { install: "./setup.sh --non-interactive" },
170
+ };
171
+ }
172
+
173
+ function setupScopeActions(scope: string): ManagedAppActionCommands {
174
+ return {
175
+ install: `aidevops setup --scope ${scope}`,
176
+ update: `aidevops setup --scope ${scope}`,
177
+ reinstall: `aidevops setup --scope ${scope}`,
178
+ };
179
+ }
180
+
181
+ function managedAppSummary(definition: ManagedAppDefinition, latestVersion: string, installedVersion: string): GuiManagedAppSummary {
182
+ const binaryPath = definition.binary === null ? null : resolveBinary(definition.binary);
183
+ const installPathRef = firstExistingPathRef(definition.install_path_refs) ?? definition.install_path_refs[0] ?? "not found";
184
+ const installedVersionText = readManagedAppVersion(definition, binaryPath, installedVersion);
185
+ const latestVersionText = definition.latest_version_source === "aidevops"
186
+ ? latestVersion
187
+ : definition.latest_version_source === "binary"
188
+ ? installedVersionText
189
+ : "unknown";
190
+
191
+ return {
192
+ id: definition.id,
193
+ name: definition.name,
194
+ description: definition.description,
195
+ category: definition.category,
196
+ origin_website_url: definition.origin_website_url,
197
+ origin_repo_url: definition.origin_repo_url,
198
+ aidevops_install: definition.aidevops_install,
199
+ aidevops_update: definition.aidevops_update,
200
+ installed_version: installedVersionText,
201
+ latest_version: latestVersionText,
202
+ install_path_ref: binaryPath === null ? installPathRef : collapseHome(binaryPath),
203
+ status: binaryPath !== null || definition.install_path_refs.some((pathRef) => existsSync(expandHome(pathRef))) ? "found" : "missing",
204
+ actions: (["install", "update", "reinstall", "remove"] as const).map((action) => managedAppAction(definition.action_commands, action)),
205
+ };
206
+ }
207
+
208
+ function managedAppAction(actionCommands: ManagedAppActionCommands, action: GuiAppActionId): GuiManagedAppSummary["actions"][number] {
209
+ return {
210
+ id: action,
211
+ label: action[0].toUpperCase() + action.slice(1),
212
+ enabled: actionCommands[action] !== undefined,
213
+ command_preview: actionCommands[action] ?? "No allowlisted command yet",
214
+ confirmation: action === "remove" ? "required" : action === "reinstall" ? "recommended" : "none",
215
+ };
216
+ }
217
+
218
+ function readManagedAppVersion(definition: ManagedAppDefinition, binaryPath: string | null, installedVersion: string): string {
219
+ if (definition.latest_version_source === "aidevops") {
220
+ return firstExistingPathRef(definition.install_path_refs) === null ? "not installed" : installedVersion;
221
+ }
222
+
223
+ if (binaryPath === null) {
224
+ return "not installed";
225
+ }
226
+
227
+ return readBinaryVersion(binaryPath, definition.version_args);
228
+ }