aidevops 3.27.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 +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/app.ts +125 -1
- package/packages/gui-api/src/status-adapter-utils.ts +35 -0
- package/packages/gui-api/src/status-adapter.ts +12 -39
- package/packages/gui-api/src/status-managed-apps.ts +228 -0
- package/packages/gui-shared/src/contracts.ts +63 -2
- package/packages/gui-shared/src/fixtures.ts +1 -0
- package/packages/gui-web/src/App.tsx +63 -1
- package/packages/gui-web/src/AppNavigation.tsx +16 -9
- package/packages/gui-web/src/AppWorkspace.tsx +95 -29
- package/packages/gui-web/src/CommandPalette.tsx +115 -22
- package/packages/gui-web/src/InventorySurfaces.tsx +107 -7
- package/packages/gui-web/src/app-model.ts +8 -11
- package/packages/gui-web/src/dashboard.ts +1 -1
- package/packages/gui-web/src/status-client.ts +1 -0
- package/packages/gui-web/src/styles.css +268 -5
- 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.
|
|
1
|
+
3.29.0
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -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
|
+
}
|