@franzzemen/brokenstock-shell 0.4.7 → 0.5.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.
|
@@ -2558,9 +2558,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2558
2558
|
args: [{ providedIn: 'root' }]
|
|
2559
2559
|
}] });
|
|
2560
2560
|
|
|
2561
|
+
/**
|
|
2562
|
+
* Names the shell handles itself, ahead of the registry. A command taking one
|
|
2563
|
+
* of these would be registered and then never reachable, so registration
|
|
2564
|
+
* refuses them outright rather than leaving a command that silently never runs.
|
|
2565
|
+
*/
|
|
2566
|
+
const RESERVED_NAMES = ['clear', 'help'];
|
|
2561
2567
|
class CommandRegistryService {
|
|
2562
2568
|
constructor() {
|
|
2563
2569
|
this.commands = new Map();
|
|
2570
|
+
this.aliases = new Map();
|
|
2564
2571
|
// Built-in shell commands registered ahead of app-provided commands so
|
|
2565
2572
|
// an app cannot accidentally shadow them.
|
|
2566
2573
|
this.register(inject(FaqCommand));
|
|
@@ -2569,8 +2576,47 @@ class CommandRegistryService {
|
|
|
2569
2576
|
this.register(cmd);
|
|
2570
2577
|
}
|
|
2571
2578
|
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Register a command and claim its names.
|
|
2581
|
+
*
|
|
2582
|
+
* ⭐ THROWS on a name already taken — real name or alternate. It used to be a
|
|
2583
|
+
* bare `set`, which meant a clash silently overwrote whichever command
|
|
2584
|
+
* registered first, and the constructor's "built-ins first so an app cannot
|
|
2585
|
+
* shadow them" comment was not actually true of the map. A shadowed command
|
|
2586
|
+
* is indistinguishable from a broken one, and this is the map dispatch reads,
|
|
2587
|
+
* so the failure belongs at startup where it cannot be missed
|
|
2588
|
+
* (scanner-command-speed.prd.md D4).
|
|
2589
|
+
*/
|
|
2572
2590
|
register(command) {
|
|
2591
|
+
this.#claim(command.name, `command '${command.name}'`);
|
|
2573
2592
|
this.commands.set(command.name, command);
|
|
2593
|
+
for (const alias of command.aliases ?? []) {
|
|
2594
|
+
this.#claim(alias.name, `alias '${alias.name}' of '${command.name}'`);
|
|
2595
|
+
this.aliases.set(alias.name, { command, prependArgs: alias.prependArgs ?? [] });
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
#claim(name, what) {
|
|
2599
|
+
if (RESERVED_NAMES.includes(name)) {
|
|
2600
|
+
throw new Error(`${what} collides with a reserved command name`);
|
|
2601
|
+
}
|
|
2602
|
+
if (this.commands.has(name) || this.aliases.has(name)) {
|
|
2603
|
+
throw new Error(`${what} collides with a command name already registered`);
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Resolve a word the user typed to the command it names — real name or
|
|
2608
|
+
* alternate. `prependArgs` is empty except for an alternate that carries a
|
|
2609
|
+
* subcommand.
|
|
2610
|
+
*
|
|
2611
|
+
* Use this wherever a TYPED word is being looked up. {@link get} deliberately
|
|
2612
|
+
* stays blind to alternates so callers that mean "the command called X" keep
|
|
2613
|
+
* meaning exactly that.
|
|
2614
|
+
*/
|
|
2615
|
+
resolve(name) {
|
|
2616
|
+
const command = this.commands.get(name);
|
|
2617
|
+
if (command)
|
|
2618
|
+
return { command, prependArgs: [] };
|
|
2619
|
+
return this.aliases.get(name);
|
|
2574
2620
|
}
|
|
2575
2621
|
get(name) {
|
|
2576
2622
|
return this.commands.get(name);
|
|
@@ -2586,11 +2632,15 @@ class CommandRegistryService {
|
|
|
2586
2632
|
if (!prefix)
|
|
2587
2633
|
return [];
|
|
2588
2634
|
if (!isAuthenticated) {
|
|
2589
|
-
const open = ['login', ...[...this.commands.values()]
|
|
2635
|
+
const open = ['login', ...[...this.commands.values()]
|
|
2636
|
+
.filter(c => c.requiresAuth === false)
|
|
2637
|
+
.flatMap(c => [c.name, ...(c.aliases ?? []).map(a => a.name)])];
|
|
2590
2638
|
return open.filter(name => name.startsWith(prefix) && name !== prefix).sort();
|
|
2591
2639
|
}
|
|
2592
2640
|
const builtIns = ['clear', 'help'];
|
|
2593
|
-
|
|
2641
|
+
// Alternate names are offered too: typing `sca` and being offered nothing
|
|
2642
|
+
// is the exact failure that produced the ref/reference pair (D5).
|
|
2643
|
+
const registered = [...this.commands.keys(), ...this.aliases.keys()];
|
|
2594
2644
|
return [...builtIns, ...registered]
|
|
2595
2645
|
.filter(name => name.startsWith(prefix) && name !== prefix)
|
|
2596
2646
|
.sort();
|
|
@@ -2678,7 +2728,10 @@ class CommandService {
|
|
|
2678
2728
|
const input = validation.data;
|
|
2679
2729
|
const parts = tokenize(input);
|
|
2680
2730
|
const command = parts[0].toLowerCase();
|
|
2681
|
-
|
|
2731
|
+
// Resolved rather than looked up by name: a command typed under an
|
|
2732
|
+
// alternate name must still get its own input sanitiser (a password line
|
|
2733
|
+
// masked under `login` stays masked under any other word for it).
|
|
2734
|
+
const def = this.registry.resolve(command)?.command;
|
|
2682
2735
|
const displayInput = def?.sanitizeInput ? def.sanitizeInput(input) : input;
|
|
2683
2736
|
const resultOrPromise = this.#dispatch(command, parts.slice(1));
|
|
2684
2737
|
this.currentInput.set('');
|
|
@@ -2770,13 +2823,30 @@ class CommandService {
|
|
|
2770
2823
|
isHelp: true,
|
|
2771
2824
|
};
|
|
2772
2825
|
}
|
|
2773
|
-
|
|
2826
|
+
/*
|
|
2827
|
+
* Resolution, not a name lookup — `scan` and `scans` reach the scanner the
|
|
2828
|
+
* same way `scanner` does. An alternate may also carry leading words
|
|
2829
|
+
* (`scans NVDA` IS `scanner select NVDA`), which are inserted ahead of what
|
|
2830
|
+
* the user typed.
|
|
2831
|
+
*
|
|
2832
|
+
* ⭐ `command` keeps the word the USER typed. Every message below
|
|
2833
|
+
* interpolates it, and telling someone who typed `scan` that the arguments
|
|
2834
|
+
* of 'scanner' are wrong reads as though something else ran. The two-word
|
|
2835
|
+
* swap is the one deliberate exception, because there the typed first word
|
|
2836
|
+
* really was an argument.
|
|
2837
|
+
*/
|
|
2838
|
+
let resolved = this.registry.resolve(command);
|
|
2839
|
+
if (resolved) {
|
|
2840
|
+
args = [...resolved.prependArgs, ...args];
|
|
2841
|
+
}
|
|
2842
|
+
let def = resolved?.command;
|
|
2774
2843
|
if (!def) {
|
|
2775
2844
|
// Try swapping the first two tokens: "disable user foo" → "user disable foo"
|
|
2776
2845
|
const swapped = args[0]?.toLowerCase();
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2846
|
+
const swappedResolved = swapped ? this.registry.resolve(swapped) : undefined;
|
|
2847
|
+
if (swappedResolved) {
|
|
2848
|
+
def = swappedResolved.command;
|
|
2849
|
+
args = [...swappedResolved.prependArgs, command, ...args.slice(1)];
|
|
2780
2850
|
command = swapped;
|
|
2781
2851
|
}
|
|
2782
2852
|
else {
|
|
@@ -2814,7 +2884,7 @@ class CommandService {
|
|
|
2814
2884
|
return { success: true, message: CLEAR_USAGE, timestamp: new Date(), isHelp: true };
|
|
2815
2885
|
if (name === 'help')
|
|
2816
2886
|
return { success: true, message: HELP_USAGE, timestamp: new Date(), isHelp: true };
|
|
2817
|
-
const def = this.registry.
|
|
2887
|
+
const def = this.registry.resolve(name)?.command;
|
|
2818
2888
|
if (!def)
|
|
2819
2889
|
return { success: false, message: `Unknown command: ${name}`, timestamp: new Date() };
|
|
2820
2890
|
// Usage is still shown for a gated command — knowing how it works is how a
|
|
@@ -2970,11 +3040,11 @@ class MainContent {
|
|
|
2970
3040
|
this.#shortcutService.helpVisible.set(false);
|
|
2971
3041
|
}
|
|
2972
3042
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: MainContent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2973
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", 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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3043
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", 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 <!--\n Esc is documented rather than added (scanner-command-speed.prd.md D24/E4).\n It has always put the cursor in the command box, from anywhere and even\n while typing in a field \u2014 it was simply never written down, which is why\n it read as a missing feature. Worded for what it actually does: it also\n closes this panel, and it does not check whether a dialog is open.\n -->\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-text-primary\">Put the cursor in the command box, from anywhere</span>\n </div>\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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
2974
3044
|
}
|
|
2975
3045
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: MainContent, decorators: [{
|
|
2976
3046
|
type: Component,
|
|
2977
|
-
args: [{ selector: 'shell-main-content', imports: [RouterOutlet, MatProgressSpinnerModule], changeDetection: ChangeDetectionStrategy.Eager, 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"] }]
|
|
3047
|
+
args: [{ selector: 'shell-main-content', imports: [RouterOutlet, MatProgressSpinnerModule], changeDetection: ChangeDetectionStrategy.Eager, 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 <!--\n Esc is documented rather than added (scanner-command-speed.prd.md D24/E4).\n It has always put the cursor in the command box, from anywhere and even\n while typing in a field \u2014 it was simply never written down, which is why\n it read as a missing feature. Worded for what it actually does: it also\n closes this panel, and it does not check whether a dialog is open.\n -->\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-text-primary\">Put the cursor in the command box, from anywhere</span>\n </div>\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"] }]
|
|
2978
3048
|
}] });
|
|
2979
3049
|
|
|
2980
3050
|
class CommandTerminal {
|