@franzzemen/brokenstock-shell 0.0.1

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.
@@ -0,0 +1,931 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, NgZone, effect, Component, viewChild, afterNextRender } from '@angular/core';
3
+ import * as i2$1 from '@angular/router';
4
+ import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
5
+ import { HttpHeader, BEARER_TOKEN_REGEX, USERNAME_REGEX, PASSWORD_REGEX } from '@franzzemen/utility';
6
+ import { validateCredentials } from '@franzzemen/identity';
7
+ import { HttpClient } from '@angular/common/http';
8
+ import { tap, catchError, from, switchMap, throwError } from 'rxjs';
9
+ import * as i3 from '@angular/forms';
10
+ import { NG_VALIDATORS, FormsModule } from '@angular/forms';
11
+ import { DOCUMENT, DatePipe } from '@angular/common';
12
+ import { z } from 'zod/v4';
13
+ import * as i1 from '@angular/material/icon';
14
+ import { MatIconModule } from '@angular/material/icon';
15
+ import * as i2 from '@angular/material/tooltip';
16
+ import { MatTooltipModule } from '@angular/material/tooltip';
17
+ import * as i1$1 from '@angular/material/progress-spinner';
18
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
19
+ import * as i5 from '@angular/cdk/text-field';
20
+ import { TextFieldModule } from '@angular/cdk/text-field';
21
+ import { MatFormField, MatLabel } from '@angular/material/form-field';
22
+ import * as i4 from '@angular/material/input';
23
+ import { MatInput, MatInputModule } from '@angular/material/input';
24
+ import * as i6 from '@angular/material/button';
25
+ import { MatButtonModule } from '@angular/material/button';
26
+ import * as i1$2 from '@angular/material/dialog';
27
+ import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
28
+
29
+ const SHELL_APP_NAME = new InjectionToken('SHELL_APP_NAME');
30
+ const SHELL_MENU_ITEMS = new InjectionToken('SHELL_MENU_ITEMS');
31
+ const SHELL_AUTH_URLS = new InjectionToken('SHELL_AUTH_URLS');
32
+ /**
33
+ * App-specific commands to register alongside the built-in command set.
34
+ * Provide via provideShellConfig().
35
+ */
36
+ const SHELL_COMMANDS = new InjectionToken('SHELL_COMMANDS');
37
+
38
+ function provideShellConfig(config) {
39
+ return makeEnvironmentProviders([
40
+ { provide: SHELL_APP_NAME, useValue: config.appName },
41
+ { provide: SHELL_MENU_ITEMS, useValue: config.menuItems },
42
+ { provide: SHELL_AUTH_URLS, useValue: config.authUrls },
43
+ { provide: SHELL_COMMANDS, useValue: config.commands ?? [] },
44
+ ]);
45
+ }
46
+
47
+ class ThinkingService {
48
+ #activeCount = signal(0, ...(ngDevMode ? [{ debugName: "#activeCount" }] : /* istanbul ignore next */ []));
49
+ isThinking = computed(() => this.#activeCount() > 0, ...(ngDevMode ? [{ debugName: "isThinking" }] : /* istanbul ignore next */ []));
50
+ startThinking() {
51
+ this.#activeCount.update(c => c + 1);
52
+ }
53
+ stopThinking() {
54
+ this.#activeCount.update(c => Math.max(0, c - 1));
55
+ }
56
+ reset() {
57
+ this.#activeCount.set(0);
58
+ }
59
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ThinkingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
60
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ThinkingService, providedIn: 'root' });
61
+ }
62
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ThinkingService, decorators: [{
63
+ type: Injectable,
64
+ args: [{ providedIn: 'root' }]
65
+ }] });
66
+
67
+ class AuthenticationService {
68
+ authenticated = signal(false, ...(ngDevMode ? [{ debugName: "authenticated" }] : /* istanbul ignore next */ []));
69
+ authenticating = signal(false, ...(ngDevMode ? [{ debugName: "authenticating" }] : /* istanbul ignore next */ []));
70
+ userSession = signal(undefined, ...(ngDevMode ? [{ debugName: "userSession" }] : /* istanbul ignore next */ []));
71
+ effectivePermissions = signal({}, ...(ngDevMode ? [{ debugName: "effectivePermissions" }] : /* istanbul ignore next */ []));
72
+ conflictSessions = signal(undefined, ...(ngDevMode ? [{ debugName: "conflictSessions" }] : /* istanbul ignore next */ []));
73
+ #refreshing = null;
74
+ #auth = 'Bearer ..';
75
+ #httpClient = inject(HttpClient);
76
+ #thinkingService = inject(ThinkingService);
77
+ #router = inject(Router);
78
+ #authUrls = inject(SHELL_AUTH_URLS);
79
+ get auth() {
80
+ return this.#auth;
81
+ }
82
+ /** Check if the current user has a specific role. */
83
+ hasRole(roleName) {
84
+ return this.authenticated() && this.userSession()?.roles.includes(roleName) === true;
85
+ }
86
+ /** Check if the current user has any of the specified roles. */
87
+ hasAnyRole(roleNames) {
88
+ return this.authenticated() && roleNames.some(r => this.userSession()?.roles.includes(r) === true);
89
+ }
90
+ refresh() {
91
+ if (this.#refreshing) {
92
+ return this.#refreshing;
93
+ }
94
+ this.#refreshing = this.#doRefresh().finally(() => { this.#refreshing = null; });
95
+ return this.#refreshing;
96
+ }
97
+ logout() {
98
+ const subscription = this.#httpClient
99
+ .get(this.#authUrls.logout, {
100
+ headers: {
101
+ [HttpHeader.Authorization]: this.#auth,
102
+ [HttpHeader.AppContext]: 'brokenstock',
103
+ },
104
+ })
105
+ .subscribe({
106
+ next: () => {
107
+ this.#clearSession();
108
+ subscription.unsubscribe();
109
+ },
110
+ error: () => {
111
+ this.#thinkingService.reset();
112
+ this.#clearSession();
113
+ },
114
+ });
115
+ }
116
+ login(credentials, force = false) {
117
+ const validation = validateCredentials(credentials);
118
+ if (validation !== true) {
119
+ return Promise.reject('Invalid credentials');
120
+ }
121
+ this.conflictSessions.set(undefined);
122
+ this.#thinkingService.startThinking();
123
+ return new Promise((resolve, reject) => {
124
+ this.authenticating.set(true);
125
+ const body = force ? { ...credentials, force: true } : credentials;
126
+ const subscription = this.#httpClient
127
+ .post(this.#authUrls.login, body, {
128
+ headers: {
129
+ [HttpHeader.ContentType]: 'application/json',
130
+ [HttpHeader.AppContext]: 'brokenstock',
131
+ },
132
+ observe: 'response',
133
+ responseType: 'json',
134
+ withCredentials: true,
135
+ })
136
+ .subscribe({
137
+ next: (response) => {
138
+ try {
139
+ const authResponse = response.body;
140
+ // @ts-ignore
141
+ delete authResponse.session.token;
142
+ this.userSession.set(authResponse.session);
143
+ this.effectivePermissions.set(authResponse.effectivePermissions ?? {});
144
+ const auth = response.headers.get(HttpHeader.Authorization);
145
+ if (!auth || !BEARER_TOKEN_REGEX.test(auth)) {
146
+ reject('Unable to login');
147
+ return;
148
+ }
149
+ this.#auth = auth;
150
+ this.authenticated.set(true);
151
+ resolve();
152
+ }
153
+ catch {
154
+ reject('Unable to login');
155
+ }
156
+ finally {
157
+ this.authenticating.set(false);
158
+ subscription.unsubscribe();
159
+ this.#thinkingService.stopThinking();
160
+ }
161
+ },
162
+ error: (err) => {
163
+ try {
164
+ if (err.status === 409) {
165
+ this.conflictSessions.set(err.error?.activeSessions ?? []);
166
+ reject('conflict');
167
+ }
168
+ else {
169
+ reject(err.message ?? 'Unable to login');
170
+ }
171
+ }
172
+ finally {
173
+ this.authenticating.set(false);
174
+ subscription.unsubscribe();
175
+ this.#thinkingService.stopThinking();
176
+ }
177
+ },
178
+ });
179
+ });
180
+ }
181
+ async #doRefresh() {
182
+ try {
183
+ const response = await fetch(this.#authUrls.refresh, {
184
+ method: 'POST',
185
+ headers: {
186
+ [HttpHeader.Authorization]: this.#auth,
187
+ [HttpHeader.AppContext]: 'brokenstock',
188
+ },
189
+ credentials: 'include',
190
+ });
191
+ if (!response.ok)
192
+ return false;
193
+ const auth = response.headers.get(HttpHeader.Authorization);
194
+ if (!auth || !BEARER_TOKEN_REGEX.test(auth))
195
+ return false;
196
+ const body = await response.json();
197
+ this.#auth = auth;
198
+ this.userSession.set(body.session);
199
+ this.effectivePermissions.set(body.effectivePermissions ?? {});
200
+ return true;
201
+ }
202
+ catch {
203
+ return false;
204
+ }
205
+ }
206
+ #clearSession() {
207
+ this.authenticated.set(false);
208
+ this.userSession.set(undefined);
209
+ this.effectivePermissions.set({});
210
+ this.conflictSessions.set(undefined);
211
+ this.#auth = 'Bearer ..';
212
+ this.#router.navigate(['/login']).catch(console.error);
213
+ }
214
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: AuthenticationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
215
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: AuthenticationService, providedIn: 'root' });
216
+ }
217
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: AuthenticationService, decorators: [{
218
+ type: Injectable,
219
+ args: [{ providedIn: 'root' }]
220
+ }] });
221
+
222
+ const authGuard = () => {
223
+ const authService = inject(AuthenticationService);
224
+ const router = inject(Router);
225
+ if (authService.authenticated()) {
226
+ return true;
227
+ }
228
+ return router.createUrlTree(['/login']);
229
+ };
230
+
231
+ const authInterceptor = (req, next) => {
232
+ const authService = inject(AuthenticationService);
233
+ const thinkingService = inject(ThinkingService);
234
+ const authUrls = inject(SHELL_AUTH_URLS);
235
+ const router = inject(Router);
236
+ return next(req).pipe(tap({
237
+ next: (event) => {
238
+ if ('headers' in event && event.headers?.get('x-permissions-stale') === 'true') {
239
+ authService.refresh();
240
+ }
241
+ },
242
+ }), catchError((error) => {
243
+ const isRefreshOrLogin = req.url === authUrls.refresh || req.url === authUrls.login;
244
+ if (error.status === 401 && !isRefreshOrLogin) {
245
+ return from(authService.refresh()).pipe(switchMap((refreshed) => {
246
+ if (refreshed) {
247
+ const retryReq = req.clone({
248
+ setHeaders: { [HttpHeader.Authorization]: authService.auth },
249
+ });
250
+ return next(retryReq);
251
+ }
252
+ thinkingService.reset();
253
+ authService.authenticated.set(false);
254
+ authService.userSession.set(undefined);
255
+ router.navigate(['/login']);
256
+ return throwError(() => error);
257
+ }), catchError(() => {
258
+ thinkingService.reset();
259
+ authService.authenticated.set(false);
260
+ authService.userSession.set(undefined);
261
+ router.navigate(['/login']);
262
+ return throwError(() => error);
263
+ }));
264
+ }
265
+ return throwError(() => error);
266
+ }));
267
+ };
268
+
269
+ class RegexValidatorDirective {
270
+ regex = input(/^[A-Z0-9._%+-@!?]{4,64}$/i, ...(ngDevMode ? [{ debugName: "regex" }] : /* istanbul ignore next */ []));
271
+ validate(control) {
272
+ if (!control.value || !this.regex().test(control.value)) {
273
+ return { regexValidator: { value: control.value } };
274
+ }
275
+ return null;
276
+ }
277
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: RegexValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
278
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.7", type: RegexValidatorDirective, isStandalone: true, selector: "[regexValidator]", inputs: { regex: { classPropertyName: "regex", publicName: "regex", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NG_VALIDATORS, useExisting: RegexValidatorDirective, multi: true }], ngImport: i0 });
279
+ }
280
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: RegexValidatorDirective, decorators: [{
281
+ type: Directive,
282
+ args: [{
283
+ selector: '[regexValidator]',
284
+ providers: [{ provide: NG_VALIDATORS, useExisting: RegexValidatorDirective, multi: true }],
285
+ }]
286
+ }], propDecorators: { regex: [{ type: i0.Input, args: [{ isSignal: true, alias: "regex", required: false }] }] } });
287
+
288
+ class ThemeService {
289
+ mode = signal('dark', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
290
+ isDark = computed(() => this.mode() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
291
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
292
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ThemeService, providedIn: 'root' });
293
+ }
294
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ThemeService, decorators: [{
295
+ type: Injectable,
296
+ args: [{ providedIn: 'root' }]
297
+ }] });
298
+
299
+ class TerminalFocusService {
300
+ focusRequested = signal(0, ...(ngDevMode ? [{ debugName: "focusRequested" }] : /* istanbul ignore next */ []));
301
+ requestFocus() {
302
+ this.focusRequested.update(v => v + 1);
303
+ }
304
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: TerminalFocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
305
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: TerminalFocusService, providedIn: 'root' });
306
+ }
307
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: TerminalFocusService, decorators: [{
308
+ type: Injectable,
309
+ args: [{ providedIn: 'root' }]
310
+ }] });
311
+
312
+ class KeyboardShortcutService {
313
+ #groups = new Map();
314
+ #activeGroupId = signal(null, ...(ngDevMode ? [{ debugName: "#activeGroupId" }] : /* istanbul ignore next */ []));
315
+ #document = inject(DOCUMENT);
316
+ #zone = inject(NgZone);
317
+ #focusService = inject(TerminalFocusService);
318
+ helpVisible = signal(false, ...(ngDevMode ? [{ debugName: "helpVisible" }] : /* istanbul ignore next */ []));
319
+ activeShortcuts = computed(() => {
320
+ const groupId = this.#activeGroupId();
321
+ if (!groupId)
322
+ return null;
323
+ const shortcuts = this.#groups.get(groupId);
324
+ if (!shortcuts)
325
+ return null;
326
+ return { groupId, shortcuts };
327
+ }, ...(ngDevMode ? [{ debugName: "activeShortcuts" }] : /* istanbul ignore next */ []));
328
+ constructor() {
329
+ this.#zone.runOutsideAngular(() => {
330
+ this.#document.addEventListener('keydown', (event) => {
331
+ this.#handleKeydown(event);
332
+ });
333
+ });
334
+ }
335
+ registerGroup(groupId, shortcuts) {
336
+ this.#groups.set(groupId, shortcuts);
337
+ this.#activeGroupId.set(groupId);
338
+ }
339
+ unregisterGroup(groupId) {
340
+ this.#groups.delete(groupId);
341
+ if (this.#activeGroupId() === groupId) {
342
+ const keys = [...this.#groups.keys()];
343
+ this.#activeGroupId.set(keys.length > 0 ? keys[keys.length - 1] : null);
344
+ }
345
+ }
346
+ toggleHelp() {
347
+ this.helpVisible.update(v => !v);
348
+ }
349
+ #handleKeydown(event) {
350
+ const activeId = this.#activeGroupId();
351
+ if (activeId) {
352
+ const shortcuts = this.#groups.get(activeId);
353
+ if (shortcuts) {
354
+ const match = shortcuts.find(s => {
355
+ if (s.modifier === 'Alt')
356
+ return event.altKey && s.key === event.key;
357
+ return !event.altKey && !event.ctrlKey && !event.metaKey && s.key === event.key;
358
+ });
359
+ if (match) {
360
+ event.preventDefault();
361
+ this.#zone.run(() => match.action());
362
+ return true;
363
+ }
364
+ }
365
+ }
366
+ if (event.key === 'Escape') {
367
+ event.preventDefault();
368
+ this.#zone.run(() => {
369
+ this.helpVisible.set(false);
370
+ this.#focusService.requestFocus();
371
+ });
372
+ return true;
373
+ }
374
+ if (!event.altKey)
375
+ return false;
376
+ if (event.key === '?' || event.key === '/') {
377
+ event.preventDefault();
378
+ this.#zone.run(() => this.toggleHelp());
379
+ return true;
380
+ }
381
+ return false;
382
+ }
383
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KeyboardShortcutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
384
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KeyboardShortcutService, providedIn: 'root' });
385
+ }
386
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: KeyboardShortcutService, decorators: [{
387
+ type: Injectable,
388
+ args: [{ providedIn: 'root' }]
389
+ }], ctorParameters: () => [] });
390
+
391
+ class CommandRegistryService {
392
+ commands = new Map();
393
+ constructor() {
394
+ const appCommands = inject(SHELL_COMMANDS, { optional: true }) ?? [];
395
+ for (const cmd of appCommands) {
396
+ this.register(cmd);
397
+ }
398
+ }
399
+ register(command) {
400
+ this.commands.set(command.name, command);
401
+ }
402
+ get(name) {
403
+ return this.commands.get(name);
404
+ }
405
+ getAll() {
406
+ return Array.from(this.commands.values());
407
+ }
408
+ /**
409
+ * Returns command name suggestions for tab-completion.
410
+ * Built-ins (clear, help) are included when authenticated.
411
+ */
412
+ getSuggestions(prefix, isAuthenticated) {
413
+ if (!prefix)
414
+ return [];
415
+ if (!isAuthenticated) {
416
+ return 'login'.startsWith(prefix) && 'login' !== prefix ? ['login'] : [];
417
+ }
418
+ const builtIns = ['clear', 'help'];
419
+ const registered = [...this.commands.values()].map(c => c.name);
420
+ return [...builtIns, ...registered]
421
+ .filter(name => name.startsWith(prefix) && name !== prefix)
422
+ .sort();
423
+ }
424
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
425
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandRegistryService, providedIn: 'root' });
426
+ }
427
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandRegistryService, decorators: [{
428
+ type: Injectable,
429
+ args: [{ providedIn: 'root' }]
430
+ }], ctorParameters: () => [] });
431
+
432
+ /**
433
+ * Quote-aware command tokenizer. Treats "..." as single tokens and strips enclosing quotes.
434
+ */
435
+ function tokenize(input) {
436
+ return (input.match(/(?:"[^"]*"|\S)+/g) ?? []).map(t => t.replace(/"([^"]*)"/g, '$1'));
437
+ }
438
+ const CLEAR_USAGE = 'clear\n\n' +
439
+ ' Clears the terminal history.\n' +
440
+ ' No arguments required.';
441
+ const HELP_USAGE = 'help [command]\n\n' +
442
+ ' With no arguments, lists all available commands.\n' +
443
+ ' With a command name, shows detailed usage for that command.\n\n' +
444
+ ' Arguments:\n' +
445
+ ' [command] Optional command name to show detailed help for';
446
+
447
+ const CommandInputSchema = z.string().trim().min(1, 'Command cannot be empty');
448
+ class CommandService {
449
+ history = signal([], ...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
450
+ currentInput = signal('', ...(ngDevMode ? [{ debugName: "currentInput" }] : /* istanbul ignore next */ []));
451
+ isProcessing = signal(false, ...(ngDevMode ? [{ debugName: "isProcessing" }] : /* istanbul ignore next */ []));
452
+ authService = inject(AuthenticationService);
453
+ registry = inject(CommandRegistryService);
454
+ constructor() {
455
+ effect(() => {
456
+ if (!this.authService.authenticated()) {
457
+ this.history.set([]);
458
+ }
459
+ });
460
+ }
461
+ lastResult = computed(() => {
462
+ const h = this.history();
463
+ return h.length > 0 ? h[h.length - 1].result : null;
464
+ }, ...(ngDevMode ? [{ debugName: "lastResult" }] : /* istanbul ignore next */ []));
465
+ echoCommand(input) {
466
+ if (this.#coalesce(input))
467
+ return;
468
+ this.history.update(h => [...h, {
469
+ input,
470
+ result: { success: true, message: '', timestamp: new Date() },
471
+ repeatCount: 1,
472
+ }]);
473
+ }
474
+ pushNotification(notification) {
475
+ this.history.update(h => [...h, {
476
+ input: '',
477
+ result: {
478
+ success: notification.severity !== 'error',
479
+ message: notification.message,
480
+ timestamp: new Date(),
481
+ severity: notification.severity,
482
+ },
483
+ }]);
484
+ }
485
+ execute(rawInput) {
486
+ const validation = CommandInputSchema.safeParse(rawInput);
487
+ if (!validation.success) {
488
+ this.history.update(h => [...h, {
489
+ input: rawInput,
490
+ result: { success: false, message: validation.error.issues[0].message, timestamp: new Date() },
491
+ }]);
492
+ this.currentInput.set('');
493
+ return;
494
+ }
495
+ const input = validation.data;
496
+ const parts = tokenize(input);
497
+ const command = parts[0].toLowerCase();
498
+ const def = this.registry.get(command);
499
+ const displayInput = def?.sanitizeInput ? def.sanitizeInput(input) : input;
500
+ const resultOrPromise = this.#dispatch(command, parts.slice(1));
501
+ this.currentInput.set('');
502
+ if (resultOrPromise instanceof Promise) {
503
+ this.isProcessing.set(true);
504
+ resultOrPromise
505
+ .then(result => {
506
+ if (!this.#coalesce(displayInput)) {
507
+ this.history.update(h => [...h, { input: displayInput, result, repeatCount: 1 }]);
508
+ }
509
+ })
510
+ .finally(() => this.isProcessing.set(false));
511
+ }
512
+ else {
513
+ if (!this.#coalesce(displayInput)) {
514
+ this.history.update(h => [...h, { input: displayInput, result: resultOrPromise, repeatCount: 1 }]);
515
+ }
516
+ }
517
+ }
518
+ #coalesce(input) {
519
+ if (!input)
520
+ return false;
521
+ const h = this.history();
522
+ for (let i = h.length - 1; i >= 0; i--) {
523
+ if (h[i].input) {
524
+ if (h[i].input === input) {
525
+ this.history.update(history => {
526
+ const updated = [...history];
527
+ updated[i] = { ...updated[i], repeatCount: (updated[i].repeatCount ?? 1) + 1 };
528
+ return updated;
529
+ });
530
+ return true;
531
+ }
532
+ return false;
533
+ }
534
+ }
535
+ return false;
536
+ }
537
+ #dispatch(command, args) {
538
+ const hasHelp = args.some(a => a.toLowerCase() === 'help');
539
+ const isAuthenticated = this.authService.authenticated();
540
+ if (!isAuthenticated && command !== 'login') {
541
+ return { success: false, message: 'Not logged in. Use "login" to authenticate.', timestamp: new Date() };
542
+ }
543
+ if (command === 'clear') {
544
+ if (hasHelp)
545
+ return { success: true, message: CLEAR_USAGE, timestamp: new Date() };
546
+ this.history.set([]);
547
+ return { success: true, message: '', timestamp: new Date() };
548
+ }
549
+ if (command === 'help') {
550
+ const target = args[0]?.toLowerCase();
551
+ if (target)
552
+ return this.#commandHelp(target);
553
+ const registered = this.registry.getAll();
554
+ const lines = [
555
+ ' clear — Clear terminal history',
556
+ ' help — List available commands',
557
+ ...registered.map(c => ` ${c.name} — ${c.description}`),
558
+ ];
559
+ return {
560
+ success: true,
561
+ message: 'Available commands:\n' + lines.join('\n') + '\n\nType "help <command>" or "<command> help" for detailed usage.',
562
+ timestamp: new Date(),
563
+ };
564
+ }
565
+ const def = this.registry.get(command);
566
+ if (!def) {
567
+ return { success: false, message: `Unknown command: ${command}`, timestamp: new Date() };
568
+ }
569
+ if (hasHelp) {
570
+ return { success: true, message: def.usage, timestamp: new Date() };
571
+ }
572
+ const validation = def.argsSchema.safeParse(args);
573
+ if (!validation.success) {
574
+ return { success: false, message: `Invalid arguments for '${command}': ${validation.error.issues[0].message}`, timestamp: new Date() };
575
+ }
576
+ return def.execute(args);
577
+ }
578
+ #commandHelp(name) {
579
+ if (name === 'clear')
580
+ return { success: true, message: CLEAR_USAGE, timestamp: new Date() };
581
+ if (name === 'help')
582
+ return { success: true, message: HELP_USAGE, timestamp: new Date() };
583
+ const def = this.registry.get(name);
584
+ if (!def)
585
+ return { success: false, message: `Unknown command: ${name}`, timestamp: new Date() };
586
+ return { success: true, message: def.usage, timestamp: new Date() };
587
+ }
588
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
589
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandService, providedIn: 'root' });
590
+ }
591
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandService, decorators: [{
592
+ type: Injectable,
593
+ args: [{ providedIn: 'root' }]
594
+ }], ctorParameters: () => [] });
595
+
596
+ class StatusBar {
597
+ #authService = inject(AuthenticationService);
598
+ #focusService = inject(TerminalFocusService);
599
+ #commandService = inject(CommandService);
600
+ isAuthenticated = this.#authService.authenticated;
601
+ loginStatus = computed(() => this.#authService.authenticating()
602
+ ? 'Authenticating...'
603
+ : this.#authService.authenticated()
604
+ ? 'Logged in'
605
+ : 'Not logged in', ...(ngDevMode ? [{ debugName: "loginStatus" }] : /* istanbul ignore next */ []));
606
+ logout() {
607
+ this.#authService.logout();
608
+ this.#commandService.echoCommand('logout');
609
+ this.#focusService.requestFocus();
610
+ }
611
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: StatusBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
612
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: StatusBar, isStandalone: true, selector: "shell-status-bar", ngImport: i0, template: "<div class=\"status-bar\">\n <div class=\"status-item\">\n <span class=\"status-label text-secondary-info\">Status:</span>\n @if (isAuthenticated()) {\n <a class=\"status-value text-interactive logout-link\" (click)=\"logout()\">Log Out</a>\n } @else {\n <span class=\"status-value text-loss\">{{ loginStatus() }}</span>\n }\n </div>\n @if (isAuthenticated()) {\n <ng-content />\n }\n</div>\n", styles: [".status-bar{display:flex;align-items:center;gap:24px;padding:0 16px;height:100%;background-color:var(--color-surface);border-bottom:1px solid var(--color-surface-border);font-size:.8rem}.status-item{display:flex;align-items:baseline;gap:6px}.status-label{font-weight:500;text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.logout-link{cursor:pointer;text-decoration:underline}.logout-link:hover{opacity:.8}\n"] });
613
+ }
614
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: StatusBar, decorators: [{
615
+ type: Component,
616
+ args: [{ selector: 'shell-status-bar', template: "<div class=\"status-bar\">\n <div class=\"status-item\">\n <span class=\"status-label text-secondary-info\">Status:</span>\n @if (isAuthenticated()) {\n <a class=\"status-value text-interactive logout-link\" (click)=\"logout()\">Log Out</a>\n } @else {\n <span class=\"status-value text-loss\">{{ loginStatus() }}</span>\n }\n </div>\n @if (isAuthenticated()) {\n <ng-content />\n }\n</div>\n", styles: [".status-bar{display:flex;align-items:center;gap:24px;padding:0 16px;height:100%;background-color:var(--color-surface);border-bottom:1px solid var(--color-surface-border);font-size:.8rem}.status-item{display:flex;align-items:baseline;gap:6px}.status-label{font-weight:500;text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.logout-link{cursor:pointer;text-decoration:underline}.logout-link:hover{opacity:.8}\n"] }]
617
+ }] });
618
+
619
+ class SideMenu {
620
+ #focusService = inject(TerminalFocusService);
621
+ #commandService = inject(CommandService);
622
+ #shortcutService = inject(KeyboardShortcutService);
623
+ hasShortcuts = this.#shortcutService.activeShortcuts;
624
+ #allMenuItems = inject(SHELL_MENU_ITEMS, { optional: true }) ?? [];
625
+ menuItems = computed(() => this.#allMenuItems.filter(item => !item.visible || item.visible()), ...(ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []));
626
+ onMenuClick(item) {
627
+ if (item.command) {
628
+ this.#commandService.echoCommand(item.command);
629
+ }
630
+ this.#focusService.requestFocus();
631
+ }
632
+ toggleHelp() {
633
+ this.#shortcutService.toggleHelp();
634
+ }
635
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SideMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
636
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: SideMenu, isStandalone: true, selector: "shell-side-menu", ngImport: i0, template: "<nav class=\"side-menu\">\n @for (item of menuItems(); track item.route) {\n <a\n class=\"menu-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [matTooltip]=\"item.label\"\n matTooltipPosition=\"right\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n </a>\n }\n <div class=\"menu-spacer\"></div>\n @if (hasShortcuts()) {\n <button\n class=\"help-button\"\n matTooltip=\"Keyboard shortcuts\"\n matTooltipPosition=\"right\"\n (click)=\"toggleHelp()\"\n >\n <span class=\"help-modifier\">Alt+</span><span class=\"help-key\">?</span>\n </button>\n }\n</nav>\n", styles: [".side-menu{display:flex;flex-direction:column;align-items:center;padding-top:8px;gap:4px;height:100%;background-color:var(--color-surface);border-right:1px solid var(--color-surface-border)}.menu-item{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:8px;color:var(--color-secondary-info);text-decoration:none;transition:background-color .15s,color .15s}.menu-item:hover,.menu-item.active{background-color:var(--color-surface-light);color:var(--color-interactive)}.menu-spacer{flex:1}.help-button{margin-bottom:8px;padding:4px 6px;border:1px solid var(--color-surface-border);border-radius:6px;background:none;font-size:.7rem;font-family:Roboto Mono,monospace;cursor:pointer;color:var(--color-interactive);transition:background-color .15s}.help-button:hover{background-color:var(--color-surface-light)}.help-modifier{opacity:.6}.help-key{font-weight:700}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
637
+ }
638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SideMenu, decorators: [{
639
+ type: Component,
640
+ args: [{ selector: 'shell-side-menu', imports: [RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule], template: "<nav class=\"side-menu\">\n @for (item of menuItems(); track item.route) {\n <a\n class=\"menu-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [matTooltip]=\"item.label\"\n matTooltipPosition=\"right\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n </a>\n }\n <div class=\"menu-spacer\"></div>\n @if (hasShortcuts()) {\n <button\n class=\"help-button\"\n matTooltip=\"Keyboard shortcuts\"\n matTooltipPosition=\"right\"\n (click)=\"toggleHelp()\"\n >\n <span class=\"help-modifier\">Alt+</span><span class=\"help-key\">?</span>\n </button>\n }\n</nav>\n", styles: [".side-menu{display:flex;flex-direction:column;align-items:center;padding-top:8px;gap:4px;height:100%;background-color:var(--color-surface);border-right:1px solid var(--color-surface-border)}.menu-item{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:8px;color:var(--color-secondary-info);text-decoration:none;transition:background-color .15s,color .15s}.menu-item:hover,.menu-item.active{background-color:var(--color-surface-light);color:var(--color-interactive)}.menu-spacer{flex:1}.help-button{margin-bottom:8px;padding:4px 6px;border:1px solid var(--color-surface-border);border-radius:6px;background:none;font-size:.7rem;font-family:Roboto Mono,monospace;cursor:pointer;color:var(--color-interactive);transition:background-color .15s}.help-button:hover{background-color:var(--color-surface-light)}.help-modifier{opacity:.6}.help-key{font-weight:700}\n"] }]
641
+ }] });
642
+
643
+ class MainContent {
644
+ #thinkingService = inject(ThinkingService);
645
+ #shortcutService = inject(KeyboardShortcutService);
646
+ isThinking = this.#thinkingService.isThinking;
647
+ shortcutHelpVisible = this.#shortcutService.helpVisible;
648
+ activeShortcuts = this.#shortcutService.activeShortcuts;
649
+ formatKey(key) {
650
+ const keyMap = {
651
+ ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→',
652
+ PageUp: 'PgUp', PageDown: 'PgDn',
653
+ };
654
+ return keyMap[key] ?? key;
655
+ }
656
+ closeShortcutHelp() {
657
+ this.#shortcutService.helpVisible.set(false);
658
+ }
659
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: MainContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
660
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: MainContent, isStandalone: true, selector: "shell-main-content", ngImport: i0, template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i1$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }] });
661
+ }
662
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: MainContent, decorators: [{
663
+ type: Component,
664
+ args: [{ selector: 'shell-main-content', imports: [RouterOutlet, MatProgressSpinnerModule], template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"] }]
665
+ }] });
666
+
667
+ class CommandTerminal {
668
+ commandService = inject(CommandService);
669
+ registry = inject(CommandRegistryService);
670
+ authService = inject(AuthenticationService);
671
+ terminalFocusService = inject(TerminalFocusService);
672
+ history = computed(() => [...this.commandService.history()].reverse(), ...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
673
+ currentInput = this.commandService.currentInput;
674
+ isProcessing = this.commandService.isProcessing;
675
+ inputEl = viewChild('terminalInput', ...(ngDevMode ? [{ debugName: "inputEl" }] : /* istanbul ignore next */ []));
676
+ tabCycleIndex = -1;
677
+ lastTabPrefix = '';
678
+ ghostText = computed(() => {
679
+ const input = this.currentInput().trim();
680
+ if (!input || input.includes(' '))
681
+ return '';
682
+ const prefix = input.toLowerCase();
683
+ const suggestions = this.registry.getSuggestions(prefix, this.authService.authenticated());
684
+ return suggestions.length > 0 ? suggestions[0] : '';
685
+ }, ...(ngDevMode ? [{ debugName: "ghostText" }] : /* istanbul ignore next */ []));
686
+ constructor() {
687
+ afterNextRender(() => this.focusInput());
688
+ effect(() => {
689
+ if (!this.isProcessing()) {
690
+ setTimeout(() => this.focusInput());
691
+ }
692
+ });
693
+ effect(() => {
694
+ this.terminalFocusService.focusRequested();
695
+ setTimeout(() => this.focusInput());
696
+ });
697
+ }
698
+ onSubmit() {
699
+ const input = this.currentInput();
700
+ if (!input.trim())
701
+ return;
702
+ this.commandService.execute(input);
703
+ this.tabCycleIndex = -1;
704
+ this.lastTabPrefix = '';
705
+ this.focusInput();
706
+ }
707
+ onInputChange(value) {
708
+ this.currentInput.set(value);
709
+ this.tabCycleIndex = -1;
710
+ this.lastTabPrefix = '';
711
+ }
712
+ onTab(event) {
713
+ event.preventDefault();
714
+ const input = this.currentInput().trim();
715
+ if (!input || input.includes(' '))
716
+ return;
717
+ const prefix = input.toLowerCase();
718
+ const suggestions = this.registry.getSuggestions(prefix, this.authService.authenticated());
719
+ if (suggestions.length === 0)
720
+ return;
721
+ if (suggestions.length === 1) {
722
+ this.currentInput.set(suggestions[0] + ' ');
723
+ this.tabCycleIndex = -1;
724
+ this.lastTabPrefix = '';
725
+ return;
726
+ }
727
+ if (this.lastTabPrefix !== prefix) {
728
+ this.lastTabPrefix = prefix;
729
+ this.tabCycleIndex = -1;
730
+ }
731
+ if (event.shiftKey) {
732
+ this.tabCycleIndex = this.tabCycleIndex <= 0 ? suggestions.length - 1 : this.tabCycleIndex - 1;
733
+ }
734
+ else {
735
+ this.tabCycleIndex = (this.tabCycleIndex + 1) % suggestions.length;
736
+ }
737
+ this.currentInput.set(suggestions[this.tabCycleIndex]);
738
+ }
739
+ getSeverityClass(result) {
740
+ if (result.severity === 'info')
741
+ return 'info';
742
+ if (result.severity === 'warning')
743
+ return 'warning';
744
+ if (result.severity === 'error' || !result.success)
745
+ return 'error';
746
+ return '';
747
+ }
748
+ focusInput() {
749
+ this.inputEl()?.nativeElement.focus();
750
+ }
751
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandTerminal, deps: [], target: i0.ɵɵFactoryTarget.Component });
752
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: CommandTerminal, isStandalone: true, selector: "shell-command-terminal", viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["terminalInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n type=\"text\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n placeholder=\"Type a command...\"\n [disabled]=\"isProcessing()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
753
+ }
754
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: CommandTerminal, decorators: [{
755
+ type: Component,
756
+ args: [{ selector: 'shell-command-terminal', imports: [FormsModule], template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n type=\"text\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n placeholder=\"Type a command...\"\n [disabled]=\"isProcessing()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}\n"] }]
757
+ }], ctorParameters: () => [], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
758
+
759
+ const MIN_TERMINAL_HEIGHT = 80;
760
+ const MAX_TERMINAL_RATIO = 0.6;
761
+ const DEFAULT_TERMINAL_HEIGHT = 160;
762
+ class Shell {
763
+ #thinkingService = inject(ThinkingService);
764
+ isThinking = this.#thinkingService.isThinking;
765
+ terminalHeight = signal(`${DEFAULT_TERMINAL_HEIGHT}px`, ...(ngDevMode ? [{ debugName: "terminalHeight" }] : /* istanbul ignore next */ []));
766
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
767
+ #startY = 0;
768
+ #startHeight = DEFAULT_TERMINAL_HEIGHT;
769
+ #onMouseMove = (event) => {
770
+ const deltaY = event.clientY - this.#startY;
771
+ const maxHeight = Math.floor(window.innerHeight * MAX_TERMINAL_RATIO);
772
+ const newHeight = Math.min(maxHeight, Math.max(MIN_TERMINAL_HEIGHT, this.#startHeight - deltaY));
773
+ this.terminalHeight.set(`${newHeight}px`);
774
+ };
775
+ #onMouseUp = () => {
776
+ this.isDragging.set(false);
777
+ document.removeEventListener('mousemove', this.#onMouseMove);
778
+ document.removeEventListener('mouseup', this.#onMouseUp);
779
+ };
780
+ onResizeStart(event) {
781
+ event.preventDefault();
782
+ this.#startY = event.clientY;
783
+ this.#startHeight = parseInt(this.terminalHeight(), 10);
784
+ this.isDragging.set(true);
785
+ document.addEventListener('mousemove', this.#onMouseMove);
786
+ document.addEventListener('mouseup', this.#onMouseUp);
787
+ }
788
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: Shell, deps: [], target: i0.ɵɵFactoryTarget.Component });
789
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.7", type: Shell, isStandalone: true, selector: "shell-root", ngImport: i0, template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\" />\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n <shell-main-content class=\"shell-main\" />\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\n</div>\n", styles: [".shell{display:grid;grid-template-areas:\"status status\" \"menu main\" \"resize resize\" \"terminal terminal\";grid-template-columns:56px 1fr;grid-template-rows:48px 1fr 4px var(--terminal-height, 160px);height:100vh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell-status{grid-area:status}.shell-menu{grid-area:menu}.shell-main{grid-area:main;overflow:hidden}.shell-resize{grid-area:resize;cursor:row-resize;background-color:var(--color-surface-border);transition:background-color .15s ease}.shell-resize:hover,.shell-resize.dragging{background-color:var(--color-interactive)}.shell-terminal{grid-area:terminal}.shell.is-dragging{user-select:none;-webkit-user-select:none}.disabled-zone{pointer-events:none;opacity:.4;transition:opacity .2s ease}\n"], dependencies: [{ kind: "component", type: StatusBar, selector: "shell-status-bar" }, { kind: "component", type: SideMenu, selector: "shell-side-menu" }, { kind: "component", type: MainContent, selector: "shell-main-content" }, { kind: "component", type: CommandTerminal, selector: "shell-command-terminal" }] });
790
+ }
791
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: Shell, decorators: [{
792
+ type: Component,
793
+ args: [{ selector: 'shell-root', imports: [StatusBar, SideMenu, MainContent, CommandTerminal], template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\" />\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n <shell-main-content class=\"shell-main\" />\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\n</div>\n", styles: [".shell{display:grid;grid-template-areas:\"status status\" \"menu main\" \"resize resize\" \"terminal terminal\";grid-template-columns:56px 1fr;grid-template-rows:48px 1fr 4px var(--terminal-height, 160px);height:100vh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell-status{grid-area:status}.shell-menu{grid-area:menu}.shell-main{grid-area:main;overflow:hidden}.shell-resize{grid-area:resize;cursor:row-resize;background-color:var(--color-surface-border);transition:background-color .15s ease}.shell-resize:hover,.shell-resize.dragging{background-color:var(--color-interactive)}.shell-terminal{grid-area:terminal}.shell.is-dragging{user-select:none;-webkit-user-select:none}.disabled-zone{pointer-events:none;opacity:.4;transition:opacity .2s ease}\n"] }]
794
+ }] });
795
+
796
+ class Login {
797
+ authenticationService;
798
+ router;
799
+ model = { username: '', password: '' };
800
+ submitted = false;
801
+ hide = signal(true, ...(ngDevMode ? [{ debugName: "hide" }] : /* istanbul ignore next */ []));
802
+ errors = [];
803
+ USERNAME_REGEX = USERNAME_REGEX;
804
+ PASSWORD_REGEX = PASSWORD_REGEX;
805
+ appName = inject(SHELL_APP_NAME, { optional: true }) ?? 'Application';
806
+ #commandService = inject(CommandService);
807
+ #terminalFocusService = inject(TerminalFocusService);
808
+ constructor(authenticationService, router) {
809
+ this.authenticationService = authenticationService;
810
+ this.router = router;
811
+ }
812
+ onAutofill(event, inputEl) {
813
+ if (event.isAutofilled) {
814
+ setTimeout(() => {
815
+ const length = inputEl.value.length;
816
+ inputEl.setSelectionRange(length, length);
817
+ });
818
+ }
819
+ }
820
+ clickEvent(event) {
821
+ this.hide.set(!this.hide());
822
+ event.stopPropagation();
823
+ }
824
+ onSubmit(event) {
825
+ this.errors = [];
826
+ event.preventDefault();
827
+ this.submitted = true;
828
+ const result = validateCredentials(this.model);
829
+ if (result === true) {
830
+ this.#doLogin(false);
831
+ }
832
+ else {
833
+ for (let ndx = 0; ndx < result.length; ndx++) {
834
+ this.errors.push({
835
+ ndx,
836
+ message: `${result[ndx].message ?? 'Unknown error'}: ${result[ndx].field ?? 'Unknown field'} ${result[ndx].type ?? 'Unknown type'} Expected: ${result[ndx].expected} Actual: ${result[ndx].actual}`,
837
+ });
838
+ }
839
+ this.submitted = false;
840
+ }
841
+ }
842
+ onTakeOver() {
843
+ this.errors = [];
844
+ this.submitted = true;
845
+ this.#doLogin(true);
846
+ }
847
+ onCancelConflict() {
848
+ this.authenticationService.conflictSessions.set(undefined);
849
+ this.submitted = false;
850
+ }
851
+ #doLogin(force) {
852
+ this.authenticationService
853
+ .login(this.model, force)
854
+ .then(() => {
855
+ this.#commandService.echoCommand(`login ${this.model.username}`);
856
+ this.router.navigate(['/']).then(() => {
857
+ setTimeout(() => this.#terminalFocusService.requestFocus());
858
+ }).catch(console.error);
859
+ })
860
+ .catch((err) => {
861
+ if (err !== 'conflict') {
862
+ this.errors.push({ ndx: 0, message: typeof err === 'string' ? err : 'Unable to login' });
863
+ }
864
+ this.submitted = false;
865
+ });
866
+ }
867
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: Login, deps: [{ token: AuthenticationService }, { token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component });
868
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: Login, isStandalone: true, selector: "shell-login", ngImport: i0, template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n </form>\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n @for (session of sessions; track $index) {\n <p class=\"text-secondary-info\">\n Already logged in from <strong>{{ session.appContext }}</strong>\n since {{ session.startEpoch | date:'short' }}.\n </p>\n }\n <p class=\"text-secondary-info\">Log in here and end that session?</p>\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n <button mat-flat-button type=\"button\" (click)=\"onTakeOver()\">Take Over</button>\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-actions{display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i5.CdkAutofill, selector: "[cdkAutofill]", outputs: ["cdkAutofill"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RegexValidatorDirective, selector: "[regexValidator]", inputs: ["regex"] }, { kind: "ngmodule", type: TextFieldModule }, { kind: "pipe", type: DatePipe, name: "date" }] });
869
+ }
870
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: Login, decorators: [{
871
+ type: Component,
872
+ args: [{ selector: 'shell-login', imports: [
873
+ FormsModule,
874
+ MatFormField,
875
+ MatLabel,
876
+ MatInput,
877
+ MatInputModule,
878
+ MatButtonModule,
879
+ MatIconModule,
880
+ RegexValidatorDirective,
881
+ TextFieldModule,
882
+ DatePipe,
883
+ ], template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n </form>\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n @for (session of sessions; track $index) {\n <p class=\"text-secondary-info\">\n Already logged in from <strong>{{ session.appContext }}</strong>\n since {{ session.startEpoch | date:'short' }}.\n </p>\n }\n <p class=\"text-secondary-info\">Log in here and end that session?</p>\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n <button mat-flat-button type=\"button\" (click)=\"onTakeOver()\">Take Over</button>\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-actions{display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}\n"] }]
884
+ }], ctorParameters: () => [{ type: AuthenticationService }, { type: i2$1.Router }] });
885
+
886
+ class ConfirmDialog {
887
+ data = inject(MAT_DIALOG_DATA);
888
+ dialogRef = inject((MatDialogRef));
889
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ConfirmDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
890
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", type: ConfirmDialog, isStandalone: true, selector: "shell-confirm-dialog", ngImport: i0, template: `
891
+ <h2 mat-dialog-title>{{ data.title }}</h2>
892
+ <mat-dialog-content>
893
+ <p>{{ data.message }}</p>
894
+ @if (data.warningMessage) {
895
+ <p class="warning-message">{{ data.warningMessage }}</p>
896
+ }
897
+ </mat-dialog-content>
898
+ <mat-dialog-actions align="end">
899
+ <button mat-button (click)="dialogRef.close(false)">{{ data.cancelLabel ?? 'Cancel' }}</button>
900
+ <button mat-flat-button color="warn" (click)="dialogRef.close(true)">{{ data.confirmLabel ?? 'Confirm' }}</button>
901
+ </mat-dialog-actions>
902
+ `, isInline: true, styles: [":host{display:block}.warning-message{color:var(--color-warning);margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }] });
903
+ }
904
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: ConfirmDialog, decorators: [{
905
+ type: Component,
906
+ args: [{ selector: 'shell-confirm-dialog', imports: [MatDialogModule, MatButtonModule], template: `
907
+ <h2 mat-dialog-title>{{ data.title }}</h2>
908
+ <mat-dialog-content>
909
+ <p>{{ data.message }}</p>
910
+ @if (data.warningMessage) {
911
+ <p class="warning-message">{{ data.warningMessage }}</p>
912
+ }
913
+ </mat-dialog-content>
914
+ <mat-dialog-actions align="end">
915
+ <button mat-button (click)="dialogRef.close(false)">{{ data.cancelLabel ?? 'Cancel' }}</button>
916
+ <button mat-flat-button color="warn" (click)="dialogRef.close(true)">{{ data.confirmLabel ?? 'Confirm' }}</button>
917
+ </mat-dialog-actions>
918
+ `, styles: [":host{display:block}.warning-message{color:var(--color-warning);margin-top:8px}\n"] }]
919
+ }] });
920
+
921
+ /*
922
+ * Public API surface for @franzzemen/brokenstock-shell
923
+ */
924
+ // Types
925
+
926
+ /**
927
+ * Generated bundle index. Do not edit.
928
+ */
929
+
930
+ export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, HELP_USAGE, KeyboardShortcutService, Login, MainContent, RegexValidatorDirective, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, Shell, SideMenu, StatusBar, TerminalFocusService, ThemeService, ThinkingService, authGuard, authInterceptor, provideShellConfig, tokenize };
931
+ //# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map