@joe-broadhead/opencode-gws 0.0.0-stage → 0.0.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/dist/tui.js ADDED
@@ -0,0 +1,589 @@
1
+ // src/tui.tsx
2
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
3
+ import { memo as _$memo } from "@opentui/solid";
4
+ import { createComponent as _$createComponent } from "@opentui/solid";
5
+ import { effect as _$effect } from "@opentui/solid";
6
+ import { insertNode as _$insertNode } from "@opentui/solid";
7
+ import { insert as _$insert } from "@opentui/solid";
8
+ import { setProp as _$setProp } from "@opentui/solid";
9
+ import { createElement as _$createElement } from "@opentui/solid";
10
+ import { TextAttributes } from "@opentui/core";
11
+ import { For, Show, createSignal } from "solid-js";
12
+
13
+ // src/rpc.ts
14
+ import { Rpc } from "@opencode/plugin/rpc";
15
+ var RPC_ID = "google-workspace.gws";
16
+ var text = { type: "string" };
17
+ var texts = { type: "array", items: text };
18
+ var object = (properties, required) => ({ type: "object", properties, required });
19
+ var MODES = ["interactive", "credentials_file", "access_token"];
20
+ var ACCESS = ["read", "write"];
21
+ var CLIENT_SOURCES = ["account", "plugin", "pasted"];
22
+ var account = object(
23
+ {
24
+ alias: text,
25
+ mode: { enum: MODES },
26
+ services: texts,
27
+ integrationID: text,
28
+ connected: { type: "boolean" },
29
+ needsAuth: { const: true },
30
+ access: { enum: ACCESS },
31
+ clientSource: { enum: CLIENT_SOURCES },
32
+ grantedScopes: texts,
33
+ missingScopes: texts,
34
+ nextStep: text
35
+ },
36
+ ["alias", "mode", "services", "connected"]
37
+ );
38
+ var status = object(
39
+ {
40
+ pluginVersion: text,
41
+ hostVersion: text,
42
+ hostWarning: text,
43
+ gws: object({ path: text, version: text }, ["path", "version"]),
44
+ services: texts,
45
+ accounts: { type: "array", items: account },
46
+ auditDegraded: { type: "boolean" },
47
+ unguardedAgents: texts,
48
+ permissionsBlock: text,
49
+ nextStep: text
50
+ },
51
+ ["pluginVersion", "hostVersion", "gws", "services", "accounts", "unguardedAgents"]
52
+ );
53
+ var Gws = Rpc.define({
54
+ id: RPC_ID,
55
+ methods: {
56
+ status: {
57
+ input: { type: "object", properties: {}, additionalProperties: false },
58
+ output: status
59
+ }
60
+ },
61
+ events: {}
62
+ });
63
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
64
+ var isStrings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
65
+ var optional = (value, check) => value === void 0 || check(value);
66
+ var isText = (value) => typeof value === "string";
67
+ function isAccount(value) {
68
+ return isRecord(value) && isText(value["alias"]) && MODES.includes(value["mode"]) && isStrings(value["services"]) && typeof value["connected"] === "boolean" && optional(value["integrationID"], isText) && optional(value["needsAuth"], (entry) => entry === true) && optional(value["access"], (entry) => ACCESS.includes(entry)) && optional(value["clientSource"], (entry) => CLIENT_SOURCES.includes(entry)) && optional(value["grantedScopes"], isStrings) && optional(value["missingScopes"], isStrings) && optional(value["nextStep"], isText);
69
+ }
70
+ function isStatus(value) {
71
+ return isRecord(value) && isText(value["pluginVersion"]) && isText(value["hostVersion"]) && optional(value["hostWarning"], isText) && isRecord(value["gws"]) && isText(value["gws"]["path"]) && isText(value["gws"]["version"]) && isStrings(value["services"]) && Array.isArray(value["accounts"]) && value["accounts"].every(isAccount) && optional(value["auditDegraded"], (entry) => typeof entry === "boolean") && isStrings(value["unguardedAgents"]) && optional(value["permissionsBlock"], isText) && optional(value["nextStep"], isText);
72
+ }
73
+ var NOTHING_TO_DO = "Nothing to do.";
74
+ function connectionLabel(account2) {
75
+ if (account2.connected) return "connected";
76
+ return account2.mode === "interactive" ? "sign-in required" : "not configured";
77
+ }
78
+ function openCodeLabel(status2) {
79
+ return status2.hostWarning ? `${status2.hostVersion} (host untested: ${status2.hostWarning})` : `${status2.hostVersion} (tested)`;
80
+ }
81
+ var BLOCK_PLACEMENT = "Put this block first in that agent's own permissions, followed by your top-level rules for gws actions. Never put it in the top-level permissions: that would loosen Explore.";
82
+
83
+ // src/tui.tsx
84
+ var TITLE = "Google Workspace: Status";
85
+ var SCOPE_PREFIX = "https://www.googleapis.com/auth/";
86
+ var REFRESH_EVENTS = ["server.connected", "integration.updated", "credential.updated", "credential.switched", "agent.updated"];
87
+ function connectionTone(account2) {
88
+ return account2.connected ? "success" : "warning";
89
+ }
90
+ function accountLabel(account2) {
91
+ const credential = account2.mode === "credentials_file" ? "credentials file" : account2.mode === "access_token" ? "access token" : "OpenCode sign-in";
92
+ return `${account2.alias} (${credential})`;
93
+ }
94
+ function accessLabel(access) {
95
+ if (access === "read") return "read-only";
96
+ if (access === "write") return "read and write";
97
+ return void 0;
98
+ }
99
+ function clientLabel(account2) {
100
+ if (account2.mode !== "interactive") return void 0;
101
+ switch (account2.clientSource) {
102
+ case "account":
103
+ return `from accounts.${account2.alias}.oauthClient`;
104
+ case "plugin":
105
+ return "from the plugin's oauthClient option";
106
+ case "pasted":
107
+ return "entered at sign-in";
108
+ default:
109
+ return "none yet";
110
+ }
111
+ }
112
+ function scopeList(scopes) {
113
+ return scopes.map((scope) => scope.startsWith(SCOPE_PREFIX) ? scope.slice(SCOPE_PREFIX.length) : scope).join(", ");
114
+ }
115
+ function Row(props) {
116
+ return (() => {
117
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("text");
118
+ _$insertNode(_el$, _el$2);
119
+ _$insertNode(_el$, _el$3);
120
+ _$setProp(_el$, "flexDirection", "row");
121
+ _$setProp(_el$, "gap", 1);
122
+ _$setProp(_el$2, "width", 29);
123
+ _$setProp(_el$2, "flexShrink", 0);
124
+ _$insert(_el$2, () => props.label);
125
+ _$setProp(_el$3, "flexGrow", 1);
126
+ _$setProp(_el$3, "wrapMode", "word");
127
+ _$insert(_el$3, () => props.value);
128
+ _$effect((_p$) => {
129
+ var _v$ = props.muted, _v$2 = props.fg;
130
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
131
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "fg", _v$2, _p$.t));
132
+ return _p$;
133
+ }, {
134
+ e: void 0,
135
+ t: void 0
136
+ });
137
+ return _el$;
138
+ })();
139
+ }
140
+ function AccountStatus(props) {
141
+ const base = () => props.colors.text.base;
142
+ const muted = () => props.colors.text.muted;
143
+ const tone = (value) => props.colors.text.feedback[value].base;
144
+ return (() => {
145
+ var _el$4 = _$createElement("box");
146
+ _$setProp(_el$4, "flexDirection", "column");
147
+ _$insert(_el$4, _$createComponent(Row, {
148
+ label: "Connection",
149
+ get value() {
150
+ return connectionLabel(props.account);
151
+ },
152
+ get fg() {
153
+ return tone(connectionTone(props.account));
154
+ },
155
+ get muted() {
156
+ return muted();
157
+ }
158
+ }), null);
159
+ _$insert(_el$4, _$createComponent(Row, {
160
+ label: "Account",
161
+ get value() {
162
+ return accountLabel(props.account);
163
+ },
164
+ get fg() {
165
+ return base();
166
+ },
167
+ get muted() {
168
+ return muted();
169
+ }
170
+ }), null);
171
+ _$insert(_el$4, _$createComponent(Show, {
172
+ get when() {
173
+ return _$memo(() => props.account.mode === "interactive")() ? accessLabel(props.account.access) : void 0;
174
+ },
175
+ children: (access) => _$createComponent(Row, {
176
+ label: "Access",
177
+ get value() {
178
+ return access();
179
+ },
180
+ get fg() {
181
+ return base();
182
+ },
183
+ get muted() {
184
+ return muted();
185
+ }
186
+ })
187
+ }), null);
188
+ _$insert(_el$4, _$createComponent(Show, {
189
+ get when() {
190
+ return clientLabel(props.account);
191
+ },
192
+ children: (client) => _$createComponent(Row, {
193
+ label: "Client",
194
+ get value() {
195
+ return client();
196
+ },
197
+ get fg() {
198
+ return base();
199
+ },
200
+ get muted() {
201
+ return muted();
202
+ }
203
+ })
204
+ }), null);
205
+ _$insert(_el$4, _$createComponent(Row, {
206
+ label: "Services",
207
+ get value() {
208
+ return props.account.services.join(", ") || "none";
209
+ },
210
+ get fg() {
211
+ return base();
212
+ },
213
+ get muted() {
214
+ return muted();
215
+ }
216
+ }), null);
217
+ _$insert(_el$4, _$createComponent(Show, {
218
+ get when() {
219
+ return _$memo(() => !!props.account.grantedScopes?.length)() ? props.account.grantedScopes : void 0;
220
+ },
221
+ children: (scopes) => _$createComponent(Row, {
222
+ label: "Granted scopes",
223
+ get value() {
224
+ return scopeList(scopes());
225
+ },
226
+ get fg() {
227
+ return base();
228
+ },
229
+ get muted() {
230
+ return muted();
231
+ }
232
+ })
233
+ }), null);
234
+ _$insert(_el$4, _$createComponent(Show, {
235
+ get when() {
236
+ return _$memo(() => !!props.account.missingScopes?.length)() ? props.account.missingScopes : void 0;
237
+ },
238
+ children: (scopes) => _$createComponent(Row, {
239
+ label: "Scopes not granted",
240
+ get value() {
241
+ return scopeList(scopes());
242
+ },
243
+ get fg() {
244
+ return tone("warning");
245
+ },
246
+ get muted() {
247
+ return muted();
248
+ }
249
+ })
250
+ }), null);
251
+ return _el$4;
252
+ })();
253
+ }
254
+ function StatusBody(props) {
255
+ const base = () => props.colors.text.base;
256
+ const muted = () => props.colors.text.muted;
257
+ const tone = (value) => props.colors.text.feedback[value].base;
258
+ return (() => {
259
+ var _el$5 = _$createElement("box"), _el$6 = _$createElement("box");
260
+ _$insertNode(_el$5, _el$6);
261
+ _$setProp(_el$5, "flexDirection", "column");
262
+ _$setProp(_el$5, "gap", 1);
263
+ _$insert(_el$5, _$createComponent(Show, {
264
+ get when() {
265
+ return props.status.accounts.length > 0;
266
+ },
267
+ get fallback() {
268
+ return _$createComponent(Row, {
269
+ label: "Connection",
270
+ value: "not configured",
271
+ get fg() {
272
+ return tone("warning");
273
+ },
274
+ get muted() {
275
+ return muted();
276
+ }
277
+ });
278
+ },
279
+ get children() {
280
+ return _$createComponent(For, {
281
+ get each() {
282
+ return props.status.accounts;
283
+ },
284
+ children: (account2) => _$createComponent(AccountStatus, {
285
+ account: account2,
286
+ get colors() {
287
+ return props.colors;
288
+ }
289
+ })
290
+ });
291
+ }
292
+ }), _el$6);
293
+ _$setProp(_el$6, "flexDirection", "column");
294
+ _$insert(_el$6, _$createComponent(Row, {
295
+ label: "Tools",
296
+ get value() {
297
+ return `gws tools for ${props.status.services.join(", ") || "no services"} (${props.status.gws.version})`;
298
+ },
299
+ get fg() {
300
+ return base();
301
+ },
302
+ get muted() {
303
+ return muted();
304
+ }
305
+ }), null);
306
+ _$insert(_el$6, _$createComponent(Show, {
307
+ get when() {
308
+ return _$memo(() => props.status.unguardedAgents.length > 0)() ? props.status.unguardedAgents : void 0;
309
+ },
310
+ get fallback() {
311
+ return _$createComponent(Row, {
312
+ label: "Agents without default rules",
313
+ value: "none",
314
+ get fg() {
315
+ return base();
316
+ },
317
+ get muted() {
318
+ return muted();
319
+ }
320
+ });
321
+ },
322
+ children: (agents) => (() => {
323
+ var _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$9 = _$createElement("text");
324
+ _$insertNode(_el$7, _el$8);
325
+ _$insertNode(_el$7, _el$9);
326
+ _$setProp(_el$7, "flexDirection", "column");
327
+ _$insert(_el$7, _$createComponent(Row, {
328
+ label: "Agents without default rules",
329
+ get value() {
330
+ return agents().join(", ");
331
+ },
332
+ get fg() {
333
+ return tone("warning");
334
+ },
335
+ get muted() {
336
+ return muted();
337
+ }
338
+ }), _el$8);
339
+ _$insert(_el$8, () => props.status.permissionsBlock ?? "");
340
+ _$setProp(_el$9, "wrapMode", "word");
341
+ _$insert(_el$9, BLOCK_PLACEMENT);
342
+ _$effect((_p$) => {
343
+ var _v$3 = base(), _v$4 = muted();
344
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$8, "fg", _v$3, _p$.e));
345
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$9, "fg", _v$4, _p$.t));
346
+ return _p$;
347
+ }, {
348
+ e: void 0,
349
+ t: void 0
350
+ });
351
+ return _el$7;
352
+ })()
353
+ }), null);
354
+ _$insert(_el$6, _$createComponent(Show, {
355
+ get when() {
356
+ return props.status.auditDegraded;
357
+ },
358
+ get children() {
359
+ return _$createComponent(Row, {
360
+ label: "Audit",
361
+ value: "some records could not be saved, so the audit log may be incomplete",
362
+ get fg() {
363
+ return tone("warning");
364
+ },
365
+ get muted() {
366
+ return muted();
367
+ }
368
+ });
369
+ }
370
+ }), null);
371
+ _$insert(_el$6, _$createComponent(Row, {
372
+ label: "OpenCode",
373
+ get value() {
374
+ return openCodeLabel(props.status);
375
+ },
376
+ get fg() {
377
+ return tone(props.status.hostWarning ? "warning" : "success");
378
+ },
379
+ get muted() {
380
+ return muted();
381
+ }
382
+ }), null);
383
+ _$insert(_el$6, _$createComponent(Row, {
384
+ label: "Next step",
385
+ get value() {
386
+ return props.status.nextStep ?? NOTHING_TO_DO;
387
+ },
388
+ get fg() {
389
+ return base();
390
+ },
391
+ get muted() {
392
+ return muted();
393
+ }
394
+ }), null);
395
+ return _el$5;
396
+ })();
397
+ }
398
+ function Dialog(props) {
399
+ props.context.keymap.layer(() => ({
400
+ mode: "modal",
401
+ commands: [{
402
+ title: "Refresh Google Workspace status",
403
+ group: "Google Workspace",
404
+ bind: "r",
405
+ run: () => props.refresh()
406
+ }, {
407
+ title: "Close Google Workspace status",
408
+ group: "Google Workspace",
409
+ bind: "escape",
410
+ run: () => props.context.ui.dialog.clear()
411
+ }]
412
+ }));
413
+ const colors = () => props.context.theme.surface("dialog");
414
+ return (() => {
415
+ var _el$0 = _$createElement("box"), _el$1 = _$createElement("box"), _el$10 = _$createElement("text"), _el$18 = _$createElement("text");
416
+ _$insertNode(_el$0, _el$1);
417
+ _$insertNode(_el$0, _el$18);
418
+ _$setProp(_el$0, "flexDirection", "column");
419
+ _$setProp(_el$0, "gap", 1);
420
+ _$setProp(_el$0, "paddingLeft", 2);
421
+ _$setProp(_el$0, "paddingRight", 2);
422
+ _$setProp(_el$0, "paddingBottom", 1);
423
+ _$insertNode(_el$1, _el$10);
424
+ _$setProp(_el$1, "flexDirection", "row");
425
+ _$setProp(_el$1, "justifyContent", "space-between");
426
+ _$insertNode(_el$10, _$createTextNode(`Google Workspace: Status`));
427
+ _$insert(_el$1, _$createComponent(Show, {
428
+ get when() {
429
+ return _$memo(() => !!props.refreshing)() && props.view.state !== "loading";
430
+ },
431
+ get children() {
432
+ var _el$12 = _$createElement("text");
433
+ _$insertNode(_el$12, _$createTextNode(`refreshing\u2026`));
434
+ _$effect((_$p) => _$setProp(_el$12, "fg", colors().text.muted, _$p));
435
+ return _el$12;
436
+ }
437
+ }), null);
438
+ _$insert(_el$0, _$createComponent(Show, {
439
+ get when() {
440
+ return props.view.state === "loading";
441
+ },
442
+ get children() {
443
+ var _el$14 = _$createElement("text");
444
+ _$insertNode(_el$14, _$createTextNode(`Loading\u2026`));
445
+ _$effect((_$p) => _$setProp(_el$14, "fg", colors().text.muted, _$p));
446
+ return _el$14;
447
+ }
448
+ }), _el$18);
449
+ _$insert(_el$0, _$createComponent(Show, {
450
+ get when() {
451
+ return props.view.state === "failed";
452
+ },
453
+ get children() {
454
+ var _el$16 = _$createElement("text");
455
+ _$insertNode(_el$16, _$createTextNode(`STATUS_UNAVAILABLE: Google Workspace status could not be read. Next step: Run /gws-status in a session for the details.`));
456
+ _$setProp(_el$16, "wrapMode", "word");
457
+ _$effect((_$p) => _$setProp(_el$16, "fg", colors().text.feedback.error.base, _$p));
458
+ return _el$16;
459
+ }
460
+ }), _el$18);
461
+ _$insert(_el$0, _$createComponent(Show, {
462
+ get when() {
463
+ return _$memo(() => props.view.state === "ready")() ? props.view.status : void 0;
464
+ },
465
+ children: (status2) => _$createComponent(StatusBody, {
466
+ get status() {
467
+ return status2();
468
+ },
469
+ get colors() {
470
+ return colors();
471
+ }
472
+ })
473
+ }), _el$18);
474
+ _$insertNode(_el$18, _$createTextNode(`r refresh \xB7 esc close`));
475
+ _$effect((_p$) => {
476
+ var _v$5 = colors().text.base, _v$6 = TextAttributes.BOLD, _v$7 = colors().text.muted;
477
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$10, "fg", _v$5, _p$.e));
478
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$10, "attributes", _v$6, _p$.t));
479
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$18, "fg", _v$7, _p$.a));
480
+ return _p$;
481
+ }, {
482
+ e: void 0,
483
+ t: void 0,
484
+ a: void 0
485
+ });
486
+ return _el$0;
487
+ })();
488
+ }
489
+ var tui_default = {
490
+ id: "google-workspace.gws.tui",
491
+ setup(context) {
492
+ const [view, setView] = createSignal({
493
+ state: "loading"
494
+ });
495
+ const [refreshing, setRefreshing] = createSignal(false);
496
+ let generation = 0;
497
+ let controller;
498
+ let open;
499
+ let disposed = false;
500
+ let unsubscribe;
501
+ const cancel = () => {
502
+ generation++;
503
+ controller?.abort();
504
+ controller = void 0;
505
+ };
506
+ const refresh = () => {
507
+ if (disposed || !open) return;
508
+ cancel();
509
+ const request = generation;
510
+ const current = new AbortController();
511
+ controller = current;
512
+ setRefreshing(true);
513
+ void context.client.rpc(Gws).status({}, {
514
+ location: context.location ?? context.data.location.default(),
515
+ signal: current.signal
516
+ }).then((status2) => isStatus(status2) ? {
517
+ state: "ready",
518
+ status: status2
519
+ } : {
520
+ state: "failed"
521
+ }, () => ({
522
+ state: "failed"
523
+ })).then((next) => {
524
+ if (request !== generation) return;
525
+ controller = void 0;
526
+ setView(next);
527
+ setRefreshing(false);
528
+ });
529
+ };
530
+ const show = () => {
531
+ if (disposed) return;
532
+ const token = {};
533
+ const stops = REFRESH_EVENTS.map((type) => context.data.on(type, refresh));
534
+ context.ui.dialog.show(() => _$createComponent(Dialog, {
535
+ context,
536
+ get view() {
537
+ return view();
538
+ },
539
+ get refreshing() {
540
+ return refreshing();
541
+ },
542
+ refresh
543
+ }), () => {
544
+ for (const stop of stops.splice(0)) stop();
545
+ if (open !== token) return;
546
+ open = void 0;
547
+ cancel();
548
+ });
549
+ context.ui.dialog.set({
550
+ size: "large"
551
+ });
552
+ open = token;
553
+ unsubscribe = () => {
554
+ for (const stop of stops.splice(0)) stop();
555
+ };
556
+ setView({
557
+ state: "loading"
558
+ });
559
+ refresh();
560
+ };
561
+ context.keymap.layer(() => ({
562
+ mode: "global",
563
+ commands: [{
564
+ id: "google-workspace.gws.status",
565
+ title: TITLE,
566
+ description: "Sign-in, access, services, unguarded agents and the next step for each Google Workspace account",
567
+ group: "Google Workspace",
568
+ palette: true,
569
+ slash: {
570
+ name: "gws"
571
+ },
572
+ run: show
573
+ }]
574
+ }));
575
+ return () => {
576
+ if (disposed) return;
577
+ disposed = true;
578
+ cancel();
579
+ unsubscribe?.();
580
+ if (open) {
581
+ open = void 0;
582
+ context.ui.dialog.clear();
583
+ }
584
+ };
585
+ }
586
+ };
587
+ export {
588
+ tui_default as default
589
+ };