aidevops 3.21.11 → 3.22.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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/aidevops.sh +3 -2
  4. package/bun.lock +573 -0
  5. package/package.json +12 -1
  6. package/packages/gui-api/package.json +9 -0
  7. package/packages/gui-api/src/app.ts +49 -0
  8. package/packages/gui-api/src/file-adapter.ts +207 -0
  9. package/packages/gui-api/src/server.ts +12 -0
  10. package/packages/gui-api/src/status-adapter-utils.ts +108 -0
  11. package/packages/gui-api/src/status-adapter.ts +484 -0
  12. package/packages/gui-api/src/status-local-repos.ts +239 -0
  13. package/packages/gui-desktop/package.json +6 -0
  14. package/packages/gui-desktop/scripts/install-macos-app.sh +1020 -0
  15. package/packages/gui-shared/package.json +9 -0
  16. package/packages/gui-shared/src/contracts.ts +341 -0
  17. package/packages/gui-shared/src/fixtures.ts +250 -0
  18. package/packages/gui-shared/src/index.ts +2 -0
  19. package/packages/gui-web/index.html +24 -0
  20. package/packages/gui-web/package.json +25 -0
  21. package/packages/gui-web/src/App.tsx +269 -0
  22. package/packages/gui-web/src/AppNavigation.tsx +379 -0
  23. package/packages/gui-web/src/AppWorkspace.tsx +114 -0
  24. package/packages/gui-web/src/FileExplorerSurface.tsx +153 -0
  25. package/packages/gui-web/src/InventorySurfaces.tsx +117 -0
  26. package/packages/gui-web/src/PathActions.tsx +17 -0
  27. package/packages/gui-web/src/StatusSurfaces.tsx +396 -0
  28. package/packages/gui-web/src/app-model.ts +546 -0
  29. package/packages/gui-web/src/dashboard.ts +79 -0
  30. package/packages/gui-web/src/main.tsx +24 -0
  31. package/packages/gui-web/src/status-client.ts +93 -0
  32. package/packages/gui-web/src/styles.css +1540 -0
  33. package/packages/gui-web/vite.config.ts +14 -0
  34. package/setup.sh +78 -3
@@ -0,0 +1,239 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import type { GuiLocalRepoSetupSummary, GuiLocalReposSetupSummary } from "../../gui-shared/src";
4
+ import {
5
+ booleanField,
6
+ collapseHome,
7
+ expandHome,
8
+ featureList,
9
+ isRecord,
10
+ readJsonObject,
11
+ readOptionalText,
12
+ safeRealpath,
13
+ stringField,
14
+ } from "./status-adapter-utils";
15
+
16
+ interface RepoCollector {
17
+ excludedWorktrees: number;
18
+ repos: Map<string, GuiLocalRepoSetupSummary>;
19
+ }
20
+
21
+ export function readLocalReposSetupSummary(reposPath: string): GuiLocalReposSetupSummary {
22
+ const registry = readJsonObject(reposPath);
23
+ const registryEntries = extractInitializedRepoEntries(registry.value);
24
+ const parentRefs = extractGitParentDirs(registry.value);
25
+ const parentDirs = parentRefs.map(expandHome).filter((pathName) => existsSync(pathName));
26
+ const collector = createRepoCollector();
27
+
28
+ addRegistryRepos(collector, registryEntries);
29
+ addParentRepos(collector, parentDirs, registryEntries);
30
+
31
+ return {
32
+ path_ref: parentRefs.join(", ") || "~/Git",
33
+ health: localReposHealth(registry.health, parentDirs, collector.repos.size),
34
+ total: collector.repos.size,
35
+ excluded_worktrees: collector.excludedWorktrees,
36
+ repos: sortedRepoSummaries(collector.repos),
37
+ };
38
+ }
39
+
40
+ function createRepoCollector(): RepoCollector {
41
+ return { excludedWorktrees: 0, repos: new Map<string, GuiLocalRepoSetupSummary>() };
42
+ }
43
+
44
+ function addRegistryRepos(collector: RepoCollector, registryEntries: Record<string, unknown>[]): void {
45
+ for (const entry of registryEntries) {
46
+ const pathRef = stringField(entry, "path");
47
+ if (pathRef !== undefined) {
48
+ addCandidateRepo(collector, expandHome(pathRef), entry);
49
+ }
50
+ }
51
+ }
52
+
53
+ function addParentRepos(
54
+ collector: RepoCollector,
55
+ parentDirs: string[],
56
+ registryEntries: Record<string, unknown>[],
57
+ ): void {
58
+ for (const parentDir of parentDirs) {
59
+ for (const childPath of listChildDirectories(parentDir)) {
60
+ addCandidateRepo(collector, childPath, matchingRegistryEntry(registryEntries, childPath));
61
+ }
62
+ }
63
+ }
64
+
65
+ function addCandidateRepo(
66
+ collector: RepoCollector,
67
+ pathName: string,
68
+ registryEntry?: Record<string, unknown>,
69
+ ): void {
70
+ if (!isGitRepoFolder(pathName)) {
71
+ return;
72
+ }
73
+ if (isLinkedWorktree(pathName)) {
74
+ collector.excludedWorktrees += 1;
75
+ return;
76
+ }
77
+ const key = safeRealpath(pathName) ?? pathName;
78
+ if (!collector.repos.has(key)) {
79
+ collector.repos.set(key, localRepoSummaryFromPath(pathName, registryEntry));
80
+ }
81
+ }
82
+
83
+ function localReposHealth(
84
+ registryHealth: "present" | "missing" | "invalid",
85
+ parentDirs: string[],
86
+ repoCount: number,
87
+ ): GuiLocalReposSetupSummary["health"] {
88
+ if (registryHealth === "invalid") {
89
+ return "invalid";
90
+ }
91
+ return parentDirs.length > 0 || repoCount > 0 ? "present" : "missing";
92
+ }
93
+
94
+ function sortedRepoSummaries(repos: Map<string, GuiLocalRepoSetupSummary>): GuiLocalRepoSetupSummary[] {
95
+ return Array.from(repos.values()).sort((left, right) => left.name.localeCompare(right.name)).slice(0, 80);
96
+ }
97
+
98
+ function localRepoSummaryFromPath(pathName: string, registryEntry?: Record<string, unknown>): GuiLocalRepoSetupSummary {
99
+ const config = readJsonObject(join(pathName, ".aidevops.json")).value;
100
+ const initConfig = isRecord(config) ? config : {};
101
+ const registry = registryEntry ?? {};
102
+ const features = featureList(initConfig.features ?? registryEntry?.features);
103
+ const remotes = readGitRemotes(pathName);
104
+
105
+ return {
106
+ name: basename(pathName),
107
+ path_ref: collapseHome(safeRealpath(pathName) ?? pathName),
108
+ aidevops_version: stringField(initConfig, "version") ?? stringField(registry, "version") ?? "not initialized",
109
+ default_branch: readGitDefaultBranch(pathName),
110
+ remotes,
111
+ registered: registryEntry !== undefined,
112
+ pulse: booleanField(registry, "pulse"),
113
+ local_only: booleanField(registry, "local_only") ?? remotes.length === 0,
114
+ init_scope: stringField(initConfig, "init_scope") ?? stringField(registry, "init_scope") ?? "unknown",
115
+ knowledge: stringField(registry, "knowledge") ?? "off",
116
+ priority: stringField(registry, "priority") ?? "default",
117
+ has_interface: booleanField(initConfig, "has_interface") ?? booleanField(registry, "has_interface"),
118
+ features,
119
+ settings_policy: "read_only_no_writes",
120
+ };
121
+ }
122
+
123
+ function extractInitializedRepoEntries(value: unknown): Record<string, unknown>[] {
124
+ if (!isRecord(value) || !Array.isArray(value.initialized_repos)) {
125
+ return [];
126
+ }
127
+ return value.initialized_repos.filter(isRecord);
128
+ }
129
+
130
+ function extractGitParentDirs(value: unknown): string[] {
131
+ if (!isRecord(value) || !Array.isArray(value.git_parent_dirs)) {
132
+ return ["~/Git"];
133
+ }
134
+ const dirs = value.git_parent_dirs.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
135
+ return dirs.length > 0 ? dirs : ["~/Git"];
136
+ }
137
+
138
+ function listChildDirectories(parentDir: string): string[] {
139
+ try {
140
+ return readdirSync(parentDir)
141
+ .map((name) => join(parentDir, name))
142
+ .filter((pathName) => {
143
+ try {
144
+ return statSync(pathName).isDirectory();
145
+ } catch {
146
+ return false;
147
+ }
148
+ });
149
+ } catch {
150
+ return [];
151
+ }
152
+ }
153
+
154
+ function isGitRepoFolder(pathName: string): boolean {
155
+ try {
156
+ if (!statSync(pathName).isDirectory()) {
157
+ return false;
158
+ }
159
+ const gitMarker = statSync(join(pathName, ".git"));
160
+ return gitMarker.isDirectory() || gitMarker.isFile();
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ function isLinkedWorktree(pathName: string): boolean {
167
+ try {
168
+ return statSync(join(pathName, ".git")).isFile();
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ function matchingRegistryEntry(entries: Record<string, unknown>[], pathName: string): Record<string, unknown> | undefined {
175
+ return entries.find((entry) => {
176
+ const entryPath = stringField(entry, "path");
177
+ return entryPath !== undefined && pathsMatch(expandHome(entryPath), pathName);
178
+ });
179
+ }
180
+
181
+ function pathsMatch(left: string, right: string): boolean {
182
+ return (safeRealpath(left) ?? left) === (safeRealpath(right) ?? right);
183
+ }
184
+
185
+ function readGitDefaultBranch(pathName: string): string {
186
+ const remoteHead = readOptionalText(join(pathName, ".git/refs/remotes/origin/HEAD"));
187
+ const remoteBranch = branchNameFromRef(remoteHead, "refs/remotes/origin/");
188
+ if (remoteBranch !== null) {
189
+ return remoteBranch;
190
+ }
191
+
192
+ const localHead = readOptionalText(join(pathName, ".git/HEAD"));
193
+ return branchNameFromRef(localHead, "refs/heads/") ?? "unknown";
194
+ }
195
+
196
+ function readGitRemotes(pathName: string): GuiLocalRepoSetupSummary["remotes"] {
197
+ const config = readOptionalText(join(pathName, ".git/config"));
198
+ if (config === null) {
199
+ return [];
200
+ }
201
+
202
+ const remotes: GuiLocalRepoSetupSummary["remotes"] = [];
203
+ let currentRemote = "";
204
+ for (const line of config.split("\n")) {
205
+ const section = line.match(/^\s*\[remote\s+"([^"]+)"\]\s*$/);
206
+ if (section !== null) {
207
+ currentRemote = section[1];
208
+ continue;
209
+ }
210
+ if (line.match(/^\s*\[/) !== null) {
211
+ currentRemote = "";
212
+ continue;
213
+ }
214
+ const url = line.match(/^\s*url\s*=\s*(.+)\s*$/);
215
+ if (url !== null && currentRemote.length > 0) {
216
+ remotes.push({ name: currentRemote, url_ref: sanitizeRemoteUrl(url[1]) });
217
+ }
218
+ }
219
+ return remotes;
220
+ }
221
+
222
+ function branchNameFromRef(value: string | null, prefix: string): string | null {
223
+ if (value === null || !value.startsWith("ref: ")) {
224
+ return null;
225
+ }
226
+ const ref = value.slice(5).trim();
227
+ return ref.startsWith(prefix) ? ref.slice(prefix.length) : null;
228
+ }
229
+
230
+ function sanitizeRemoteUrl(value: string): string {
231
+ try {
232
+ const parsed = new URL(value);
233
+ parsed.username = "";
234
+ parsed.password = "";
235
+ return parsed.toString().replace(/\/$/, "");
236
+ } catch {
237
+ return value.replace(/(https?:\/\/)[^/@\s]+@/i, "$1");
238
+ }
239
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@aidevops/gui-desktop",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true
6
+ }