@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()].filter(c => c.requiresAuth === false).map(c => c.name)];
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
- const registered = [...this.commands.values()].map(c => c.name);
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
- const def = this.registry.get(command);
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
- let def = this.registry.get(command);
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
- if (swapped && this.registry.get(swapped)) {
2778
- def = this.registry.get(swapped);
2779
- args = [command, ...args.slice(1)];
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.get(name);
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 {