aidevops 3.21.12 → 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,396 @@
1
+ /* jshint esversion: 11 */
2
+ import { useState } from "react";
3
+ import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiOAuthProviderSummary, GuiSetupTargetSummary, GuiStatusData } from "../../gui-shared/src";
4
+ import { plannedHomes, text } from "./app-model";
5
+ import { FileExplorerSurface } from "./FileExplorerSurface";
6
+ import { PathActions } from "./PathActions";
7
+
8
+ export function OverviewSurface({ status }: { status: GuiStatusData }) {
9
+ const metrics = [
10
+ { label: text.setup, value: status.update.restart_required ? "restart" : "current", detail: status.update.installed_version },
11
+ { label: text.projects, value: String(status.repos.total), detail: status.repos.health },
12
+ { label: text.config, value: String(status.settings.key_count), detail: status.settings.value_policy },
13
+ { label: text.security, value: String(status.secrets.length), detail: "secret references" },
14
+ ];
15
+
16
+ return (
17
+ <section className="surface-page" aria-label="Overview">
18
+ <div className="hero-panel compact-hero">
19
+ <p className="eyebrow">{text.roadmapIntro}</p>
20
+ <h2>{text.aidevops}</h2>
21
+ <p>{text.repoDescription}</p>
22
+ </div>
23
+ <div className="metric-grid">
24
+ {metrics.map((metric) => (
25
+ <article className="metric-card" key={metric.label}>
26
+ <span>{metric.label}</span>
27
+ <strong>{metric.value}</strong>
28
+ <small>{metric.detail}</small>
29
+ </article>
30
+ ))}
31
+ </div>
32
+ <section className="panel">
33
+ <div className="section-heading">
34
+ <p className="eyebrow">{text.plannedHomes}</p>
35
+ <h2>{text.fileBrowser}</h2>
36
+ </div>
37
+ <div className="planned-home-grid">
38
+ {plannedHomes.map((home) => (
39
+ <article className="planned-home" key={home.area}>
40
+ <span>{home.phase}</span>
41
+ <strong>{home.area}</strong>
42
+ <small>{home.home}</small>
43
+ </article>
44
+ ))}
45
+ </div>
46
+ </section>
47
+ <section className="panel">
48
+ <div className="section-heading">
49
+ <p className="eyebrow">{text.path}</p>
50
+ <h2>{text.localSetup}</h2>
51
+ </div>
52
+ <ul className="object-list">
53
+ {status.paths.map((path) => (
54
+ <li key={path.label}>
55
+ <strong>{path.label}</strong>
56
+ <span>{path.health}</span>
57
+ <code>{path.path_ref}</code>
58
+ <PathActions pathRef={path.path_ref} />
59
+ </li>
60
+ ))}
61
+ </ul>
62
+ </section>
63
+ <SetupTargetsPanel status={status} />
64
+ <AiAppsPanel status={status} />
65
+ </section>
66
+ );
67
+ }
68
+
69
+ export function ProjectsSurface({ status }: { status: GuiStatusData }) {
70
+ return (
71
+ <section className="panel" aria-label={text.projects}>
72
+ <div className="section-heading split-heading">
73
+ <div>
74
+ <p className="eyebrow">{text.development}</p>
75
+ <h2>{text.projects}</h2>
76
+ <p>{status.repos.path_ref}</p>
77
+ </div>
78
+ <PathActions pathRef={status.repos.path_ref} />
79
+ </div>
80
+ {status.repos.repos.length === 0 ? (
81
+ <p className="empty-state">{text.noProjectEntries}</p>
82
+ ) : (
83
+ <ul className="object-list">
84
+ {status.repos.repos.map((repo) => (
85
+ <li key={`${repo.platform}:${repo.slug}`}>
86
+ <strong>{repo.name}</strong>
87
+ <span>{repo.platform}</span>
88
+ <small>{repo.slug}</small>
89
+ <small>{repo.local_path_status}</small>
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ )}
94
+ </section>
95
+ );
96
+ }
97
+
98
+ export function SecuritySurface({ status }: { status: GuiStatusData }) {
99
+ return (
100
+ <section className="panel" aria-label={text.security}>
101
+ <div className="section-heading">
102
+ <p className="eyebrow">{text.readOnly}</p>
103
+ <h2>{text.security}</h2>
104
+ <p>{text.securityBoundary}</p>
105
+ </div>
106
+ <ul className="object-list">
107
+ {status.secrets.map((secret) => (
108
+ <li key={secret.name}>
109
+ <strong>{secret.name}</strong>
110
+ <span>{secret.status}</span>
111
+ </li>
112
+ ))}
113
+ </ul>
114
+ </section>
115
+ );
116
+ }
117
+
118
+ export function LocalReposSurface({ status }: { status: GuiStatusData }) {
119
+ const [activeTab, setActiveTab] = useState<"setup" | "files">("setup");
120
+
121
+ return (
122
+ <section className="surface-page local-repos-surface" aria-label={text.localRepos}>
123
+ <nav className="pill-tabs" aria-label="Local Repos views">
124
+ <button aria-pressed={activeTab === "setup"} className={activeTab === "setup" ? "active" : ""} onClick={() => setActiveTab("setup")} type="button">
125
+ {text.setup}
126
+ </button>
127
+ <button aria-pressed={activeTab === "files"} className={activeTab === "files" ? "active" : ""} onClick={() => setActiveTab("files")} type="button">
128
+ File Explorer
129
+ </button>
130
+ </nav>
131
+ {activeTab === "setup" ? <LocalReposSetupPanel status={status} /> : <FileExplorerSurface rootId="git" />}
132
+ </section>
133
+ );
134
+ }
135
+
136
+ function LocalReposSetupPanel({ status }: { status: GuiStatusData }) {
137
+ return (
138
+ <section className="panel repo-setup-panel" aria-label="Local repo setup">
139
+ <div className="section-heading split-heading">
140
+ <div>
141
+ <p className="eyebrow">{status.local_repos.path_ref}</p>
142
+ <h2>{text.setup}</h2>
143
+ <p>{text.localReposSetupIntro}</p>
144
+ </div>
145
+ <span className="count-pill">{status.local_repos.total} repos</span>
146
+ </div>
147
+ <div className="repo-summary-strip">
148
+ <span>{status.local_repos.health}</span>
149
+ <span>{status.local_repos.excluded_worktrees} worktrees excluded</span>
150
+ <span>repos.json: {status.repos.health}</span>
151
+ </div>
152
+ {status.local_repos.repos.length === 0 ? (
153
+ <p className="empty-state">No canonical local repo folders were discovered.</p>
154
+ ) : (
155
+ <div className="repo-card-grid">
156
+ {status.local_repos.repos.map((repo) => <LocalRepoCard key={repo.path_ref} repo={repo} />)}
157
+ </div>
158
+ )}
159
+ </section>
160
+ );
161
+ }
162
+
163
+ function LocalRepoCard({ repo }: { repo: GuiLocalRepoSetupSummary }) {
164
+ const pulseLabel = repo.pulse === null ? "not registered" : repo.pulse ? "included" : "excluded";
165
+
166
+ return (
167
+ <article className="repo-setup-card">
168
+ <header>
169
+ <div>
170
+ <h3>{repo.name}</h3>
171
+ <code>{repo.path_ref}</code>
172
+ </div>
173
+ <PathActions pathRef={repo.path_ref} />
174
+ </header>
175
+ <div className="repo-card-meta">
176
+ <Detail label="aidevops init" value={repo.aidevops_version} />
177
+ <Detail label="default branch" value={repo.default_branch} />
178
+ <Detail label="init scope" value={repo.init_scope} />
179
+ <Detail label="knowledge" value={repo.knowledge} />
180
+ <Detail label="priority" value={repo.priority} />
181
+ <Detail label="interface" value={repo.has_interface === null ? "unknown" : repo.has_interface ? "yes" : "no"} />
182
+ </div>
183
+ <div className="repo-remotes">
184
+ <span>Remote copy paths</span>
185
+ {repo.remotes.length === 0 ? <small>none configured</small> : repo.remotes.map((remote) => <code key={`${remote.name}:${remote.url_ref}`}>{remote.name}: {remote.url_ref}</code>)}
186
+ </div>
187
+ <div className="repo-card-footer">
188
+ <label className="switch-control" title="Read-only preview; changing Pulse requires an audited repos.json write route.">
189
+ <input checked={repo.pulse === true} disabled type="checkbox" />
190
+ <span aria-hidden="true" />
191
+ <strong>Pulse</strong>
192
+ </label>
193
+ <small>{pulseLabel}</small>
194
+ <small>{repo.local_only ? "local only" : repo.registered ? "registered" : "unregistered"}</small>
195
+ </div>
196
+ {repo.features.length > 0 ? (
197
+ <div className="tag-list repo-tags">
198
+ {repo.features.slice(0, 6).map((feature) => <span key={feature}>{feature}</span>)}
199
+ </div>
200
+ ) : null}
201
+ </article>
202
+ );
203
+ }
204
+
205
+ function SetupTargetsPanel({ status }: { status: GuiStatusData }) {
206
+ const needsUpdate = status.setup_targets.filter((target) => target.needs_update).length;
207
+
208
+ return (
209
+ <section className="panel setup-targets-panel" aria-label={text.setupTargets}>
210
+ <div className="section-heading split-heading">
211
+ <div>
212
+ <p className="eyebrow">{text.localSetup}</p>
213
+ <h2>{text.setupTargets}</h2>
214
+ <p>{text.setupTargetsIntro}</p>
215
+ </div>
216
+ <span className="count-pill">{needsUpdate} need update</span>
217
+ </div>
218
+ {status.setup_targets.length === 0 ? (
219
+ <p className="empty-state">No aidevops setup targets were reported.</p>
220
+ ) : (
221
+ <div className="setup-card-grid">
222
+ {status.setup_targets.map((target) => <SetupTargetCard key={target.path_ref} target={target} />)}
223
+ </div>
224
+ )}
225
+ </section>
226
+ );
227
+ }
228
+
229
+ function SetupTargetCard({ target }: { target: GuiSetupTargetSummary }) {
230
+ return (
231
+ <article className="setup-target-card">
232
+ <header>
233
+ <div>
234
+ <p className="eyebrow">{target.health}</p>
235
+ <h3>{target.label}</h3>
236
+ </div>
237
+ <span className={target.needs_update ? "status-pill warn" : "status-pill"}>{target.needs_update ? "update" : "current"}</span>
238
+ </header>
239
+ <p>{target.purpose}</p>
240
+ <div className="repo-card-meta">
241
+ <Detail label="installed" value={target.installed_version} />
242
+ <Detail label="latest" value={target.latest_version} />
243
+ </div>
244
+ <div className="setup-path-row">
245
+ <code>{target.path_ref}</code>
246
+ <MaybePathActions pathRef={target.path_ref} />
247
+ </div>
248
+ </article>
249
+ );
250
+ }
251
+
252
+ function AiAppsPanel({ status }: { status: GuiStatusData }) {
253
+ const foundApps = status.ai_apps.filter((app) => app.status === "found").length;
254
+
255
+ return (
256
+ <section className="panel ai-apps-panel" aria-label={text.aiApps}>
257
+ <div className="section-heading split-heading">
258
+ <div>
259
+ <p className="eyebrow">{text.localSetup}</p>
260
+ <h2>{text.aiApps}</h2>
261
+ <p>{text.aiAppsIntro}</p>
262
+ </div>
263
+ <span className="count-pill">{foundApps}/{status.ai_apps.length} found</span>
264
+ </div>
265
+ {status.ai_apps.length === 0 ? (
266
+ <p className="empty-state">No local AI app metadata was reported.</p>
267
+ ) : (
268
+ <div className="ai-app-grid">
269
+ {status.ai_apps.map((app) => <AiAppCard app={app} key={app.name} />)}
270
+ </div>
271
+ )}
272
+ </section>
273
+ );
274
+ }
275
+
276
+ function AiAppCard({ app }: { app: GuiAiAppSummary }) {
277
+ return (
278
+ <article className="ai-app-card">
279
+ <header>
280
+ <div>
281
+ <p className="eyebrow">{app.status}</p>
282
+ <h3>{app.name}</h3>
283
+ </div>
284
+ <span className={app.needs_update ? "status-pill warn" : "status-pill"}>{app.needs_update ? "update" : "current"}</span>
285
+ </header>
286
+ <div className="repo-card-meta">
287
+ <Detail label="app version" value={app.app_version} />
288
+ <Detail label="aidevops" value={app.aidevops_version} />
289
+ <Detail label="latest" value={app.latest_version} />
290
+ </div>
291
+ <div className="setup-path-list">
292
+ <PathRow label="app" pathRef={app.app_path_ref} />
293
+ <PathRow label="binary" pathRef={app.binary_path_ref} />
294
+ <PathRow label="config" pathRef={app.config_path_ref} />
295
+ <PathRow label="aidevops" pathRef={app.aidevops_target_path_ref} />
296
+ </div>
297
+ </article>
298
+ );
299
+ }
300
+
301
+ export function AiProvidersSurface({ status }: { status: GuiStatusData }) {
302
+ return (
303
+ <section className="panel ai-providers-panel" aria-label={text.aiProviders}>
304
+ <div className="section-heading split-heading">
305
+ <div>
306
+ <p className="eyebrow">{status.oauth_pool.path_ref}</p>
307
+ <h2>{text.aiProviders}</h2>
308
+ <p>{text.aiProvidersIntro}</p>
309
+ </div>
310
+ <span className="count-pill">{status.oauth_pool.value_policy}</span>
311
+ </div>
312
+ <div className="provider-grid">
313
+ {status.oauth_pool.providers.map((provider) => <ProviderCard key={provider.provider} provider={provider} />)}
314
+ </div>
315
+ </section>
316
+ );
317
+ }
318
+
319
+ function ProviderCard({ provider }: { provider: GuiOAuthProviderSummary }) {
320
+ return (
321
+ <article className="provider-card">
322
+ <header>
323
+ <div>
324
+ <p className="eyebrow">{provider.configured ? "configured" : "not configured"}</p>
325
+ <h3>{provider.provider}</h3>
326
+ </div>
327
+ <strong>{provider.available}/{provider.total}</strong>
328
+ </header>
329
+ <div className="repo-card-meta provider-metrics">
330
+ <Detail label="active/idle" value={String(provider.active_or_idle)} />
331
+ <Detail label="rate limited" value={String(provider.rate_limited)} />
332
+ <Detail label="auth errors" value={String(provider.auth_errors)} />
333
+ <Detail label="pending token" value={provider.pending_token ? "yes" : "no"} />
334
+ </div>
335
+ <fieldset className="provider-actions" aria-label={`${provider.provider} planned management controls`}>
336
+ {['Add account', 'Rotate', 'Check', 'Reset cooldowns'].map((action) => <button disabled key={action} type="button">{action}</button>)}
337
+ </fieldset>
338
+ {provider.accounts.length === 0 ? (
339
+ <p className="empty-state compact-empty">No accounts in this pool.</p>
340
+ ) : (
341
+ <ul className="provider-account-list">
342
+ {provider.accounts.map((account) => (
343
+ <li key={`${provider.provider}:${account.email_ref}`}>
344
+ <strong>{account.email_ref}</strong>
345
+ <span>{account.status}</span>
346
+ <small>priority {account.priority ?? "default"}</small>
347
+ <small>expires {account.expires_at}</small>
348
+ {account.cooldown_until ? <small>cooldown {account.cooldown_until}</small> : null}
349
+ <button disabled type="button">Manage</button>
350
+ </li>
351
+ ))}
352
+ </ul>
353
+ )}
354
+ </article>
355
+ );
356
+ }
357
+
358
+ function Detail({ label, value }: { label: string; value: string }) {
359
+ return (
360
+ <span>
361
+ <small>{label}</small>
362
+ <strong>{value}</strong>
363
+ </span>
364
+ );
365
+ }
366
+
367
+ function PathRow({ label, pathRef }: { label: string; pathRef: string }) {
368
+ return (
369
+ <div className="setup-path-row">
370
+ <small>{label}</small>
371
+ <code>{pathRef}</code>
372
+ <MaybePathActions pathRef={pathRef} />
373
+ </div>
374
+ );
375
+ }
376
+
377
+ function MaybePathActions({ pathRef }: { pathRef: string }) {
378
+ return isActionablePathRef(pathRef) ? <PathActions pathRef={pathRef} /> : null;
379
+ }
380
+
381
+ function isActionablePathRef(pathRef: string): boolean {
382
+ return pathRef.startsWith("~/") || pathRef.startsWith("/");
383
+ }
384
+
385
+ export function PlannedSurface({ detail, label }: { detail: string; label: string }) {
386
+ return (
387
+ <section className="panel" aria-label={label}>
388
+ <div className="section-heading">
389
+ <p className="eyebrow">{text.planned}</p>
390
+ <h2>{label}</h2>
391
+ <p>{detail}</p>
392
+ </div>
393
+ <p className="empty-state">{text.plannedNotice}</p>
394
+ </section>
395
+ );
396
+ }