tabby-profile-bridge 0.0.0-stage → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/README.pt-BR.md +73 -0
- package/dist/bridge.component.d.ts +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/settingsProvider.d.ts +8 -0
- package/package.json +61 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.2.2 — Bilingual interface and documentation
|
|
4
|
+
- English-default UI with Portuguese translation following Tabby's effective locale.
|
|
5
|
+
- Live updates for pt-BR and pt-PT, including automatic Tabby language selection.
|
|
6
|
+
- Top-right by gugahartmann · vX.Y.Z attribution with version from the installed package.
|
|
7
|
+
- English and Brazilian Portuguese user documentation.
|
|
8
|
+
- README focused on features and usage.
|
|
9
|
+
|
|
10
|
+
## v0.2.1 — Angular template hotfix
|
|
11
|
+
- Fix invalid inline arrow expression in Angular settings template.
|
|
12
|
+
- Add Angular template parsing regression coverage.
|
|
13
|
+
|
|
14
|
+
## v0.2.0 — MobaXterm
|
|
15
|
+
- Add .mxtsessions import/export and nested folder support for SSH/Telnet.
|
|
16
|
+
- Support Windows-1252 and CRLF encoding and warn about incompatible settings.
|
|
17
|
+
|
|
18
|
+
## v0.1.0 — CSV
|
|
19
|
+
- Add CSV import/export, nested group selection, profile conflict review, and bulk username editing.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gugahartmann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tabby Profile Bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Your connections, across terminals.**
|
|
4
|
+
|
|
5
|
+
Import and export SSH/Telnet profiles across Tabby, CSV, and MobaXterm while preserving nested folders.
|
|
6
|
+
|
|
7
|
+
**by gugahartmann** · [Português (Brasil)](README.pt-BR.md)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **CSV and MobaXterm** import/export (.csv and .mxtsessions).
|
|
12
|
+
- **Nested folders**: select entire groups or individual sessions, retaining only necessary ancestor folders.
|
|
13
|
+
- **Export destination**: preserve the root hierarchy or wrap selected sessions in a named parent folder.
|
|
14
|
+
- **Per-file import destination**: import every source file into the root or its own parent folder.
|
|
15
|
+
- **Conflict preview**: inspect the hostname, port, username and existing matches before choosing Keep / skip, Add as new, or Replace existing. Matching profiles are skipped by default.
|
|
16
|
+
- **Bulk username conversion** before import, plus individual username editing.
|
|
17
|
+
- **Privacy-first export**: usernames are excluded unless expressly selected. Passwords, private-key content/paths, login scripts, and proxy commands are not exported.
|
|
18
|
+
- **Bilingual interface**: defaults to English and follows Tabby's selected Portuguese (pt-BR or pt-PT) locale, including Automatic language detection.
|
|
19
|
+
- **Version attribution** at the upper-right of the plugin screen: by gugahartmann · vX.Y.Z.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
After the package is published on npm, find **tabby-profile-bridge** in Tabby → Settings → Plugins. You can also obtain an installable .tgz from [GitHub Releases](https://github.com/gugahartmann/Tabby-Profile-Bridge/releases), using a plugin installation workflow supported by your Tabby version.
|
|
24
|
+
|
|
25
|
+
Open **Settings → Profile Bridge** after enabling the plugin.
|
|
26
|
+
|
|
27
|
+
## Export
|
|
28
|
+
|
|
29
|
+
1. Select the **Export** tab and tick the groups or individual SSH/Telnet sessions.
|
|
30
|
+
2. Opt in to the usernames that may be included in the export.
|
|
31
|
+
3. Select **Preserve hierarchy at root** or a named **parent folder**.
|
|
32
|
+
4. Choose **CSV** or **MobaXterm** and export.
|
|
33
|
+
|
|
34
|
+
## Import
|
|
35
|
+
|
|
36
|
+
1. Select the **Import** tab and add one or more CSV or MobaXterm files.
|
|
37
|
+
2. For **each file**, select import at the root or inside its own parent folder.
|
|
38
|
+
3. Review the imported sessions and warnings.
|
|
39
|
+
4. Optionally replace legacy usernames in bulk or edit them individually.
|
|
40
|
+
5. Choose **Keep / skip**, **Add as new**, or **Replace existing** for each session before applying changes.
|
|
41
|
+
|
|
42
|
+
Matching name + protocol + folder defaults to Keep / skip. The same endpoint under another name or folder is flagged without automatic replacement. Back up Tabby before replacing existing connections.
|
|
43
|
+
|
|
44
|
+
## CSV fields
|
|
45
|
+
|
|
46
|
+
| group1 | group2 | other_groups | profile | protocol | hostname | port | username |
|
|
47
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
48
|
+
| Region | City | POP / Core | CORE-01 | ssh | 192.0.2.10 | 30022 | netops |
|
|
49
|
+
| Region | City | POP / Access | OLT-01 | ssh | 192.0.2.11 | 22 | |
|
|
50
|
+
|
|
51
|
+
Additional columns: **auth**, **algorithms**, and **settings** (safe JSON strings). The first two folder levels have dedicated columns. Deeper paths are encoded in other_groups with escaped slash delimiters. The IPs shown above are reserved example addresses.
|
|
52
|
+
|
|
53
|
+
## MobaXterm compatibility
|
|
54
|
+
|
|
55
|
+
Supports compatible SSH/Telnet bookmarks in .mxtsessions with names, nested folders, hostname/IP, custom ports, usernames, and basic X11. Advanced MobaXterm-specific settings may be omitted and unsupported data can be reported. Verify exported files in the target MobaXterm version before production use.
|
|
56
|
+
|
|
57
|
+
## Security and limits
|
|
58
|
+
|
|
59
|
+
Processing is local. Hostnames, IPs, and opt-in usernames may still be sensitive, so inspect files before sharing. Passwords, private-key content or paths, scripts, and proxy commands are never exported. Replacing a profile discards its previous connection-specific credentials.
|
|
60
|
+
|
|
61
|
+
Import limit: **5 MB/file**, **10,000 profiles/file**, **32 folder levels**. MobaXterm may reject folder/session characters not supported by its format.
|
|
62
|
+
|
|
63
|
+
## Documentation
|
|
64
|
+
|
|
65
|
+
- [User Guide (English)](docs/USER_GUIDE.md)
|
|
66
|
+
- [Guia do Usuário (Português do Brasil)](docs/GUIA_USUARIO.pt-BR.md)
|
|
67
|
+
- [Changelog](CHANGELOG.md)
|
|
68
|
+
|
|
69
|
+
## Roadmap
|
|
70
|
+
|
|
71
|
+
Future adapters: SecureCRT, mRemoteNG and other connection managers.
|
|
72
|
+
|
|
73
|
+
MIT © 2026 gugahartmann.
|
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Tabby Profile Bridge
|
|
2
|
+
|
|
3
|
+
**Suas conexões, em diferentes terminais.**
|
|
4
|
+
|
|
5
|
+
Importe e exporte perfis SSH/Telnet entre o Tabby, arquivos CSV e o MobaXterm, preservando a hierarquia das pastas.
|
|
6
|
+
|
|
7
|
+
**by gugahartmann** · [English](README.md)
|
|
8
|
+
|
|
9
|
+
## Funcionalidades
|
|
10
|
+
|
|
11
|
+
- **CSV e MobaXterm**: importação e exportação de .csv e .mxtsessions.
|
|
12
|
+
- **Pastas aninhadas**: escolha grupos completos ou sessões individuais, mantendo os grupos ancestrais necessários.
|
|
13
|
+
- **Destino da exportação**: mantenha a hierarquia na raiz ou coloque as sessões em uma pasta principal.
|
|
14
|
+
- **Destino por arquivo**: cada arquivo importado pode ir para a raiz ou para sua própria pasta principal.
|
|
15
|
+
- **Prévia de conflitos**: revise hostname, porta, usuário e possíveis correspondências antes de escolher Manter / ignorar, Adicionar como novo ou Substituir existente. O padrão é não substituir.
|
|
16
|
+
- **Alteração de usuários em massa** antes da importação ou edição individual.
|
|
17
|
+
- **Privacidade**: nomes de usuário só entram na exportação se selecionados. Senhas, chaves privadas, caminhos de chaves, scripts de login e comandos de proxy não são exportados.
|
|
18
|
+
- **Interface bilíngue**: inglês por padrão e português quando o Tabby utiliza pt-BR ou pt-PT, inclusive com detecção automática de idioma.
|
|
19
|
+
- **Créditos e versão** no canto superior direito: by gugahartmann · vX.Y.Z.
|
|
20
|
+
|
|
21
|
+
## Instalação
|
|
22
|
+
|
|
23
|
+
Após a publicação no npm, procure **tabby-profile-bridge** em Tabby → Configurações → Plugins. Também é possível obter o pacote .tgz nas [releases do GitHub](https://github.com/gugahartmann/Tabby-Profile-Bridge/releases) e usar um procedimento de instalação de plugins compatível com sua versão do Tabby.
|
|
24
|
+
|
|
25
|
+
Após ativar o plugin, abra **Configurações → Profile Bridge**.
|
|
26
|
+
|
|
27
|
+
## Exportação
|
|
28
|
+
|
|
29
|
+
1. Na aba **Exportar**, marque os grupos ou perfis SSH/Telnet desejados.
|
|
30
|
+
2. Selecione explicitamente quais usuários podem constar no arquivo.
|
|
31
|
+
3. Escolha **Preservar hierarquia na raiz** ou definir uma **pasta principal**.
|
|
32
|
+
4. Selecione **CSV** ou **MobaXterm** e exporte.
|
|
33
|
+
|
|
34
|
+
## Importação
|
|
35
|
+
|
|
36
|
+
1. Na aba **Importar**, selecione um ou mais arquivos CSV ou MobaXterm.
|
|
37
|
+
2. Para **cada arquivo**, escolha importar na raiz ou dentro de uma pasta principal.
|
|
38
|
+
3. Revise os perfis encontrados e os avisos.
|
|
39
|
+
4. Se desejar, altere usuários antigos em massa ou individualmente.
|
|
40
|
+
5. Para cada perfil, escolha **Manter / ignorar**, **Adicionar como novo** ou **Substituir existente**, e aplique as alterações.
|
|
41
|
+
|
|
42
|
+
Uma correspondência entre nome, protocolo e pasta começa como Manter / ignorar. Endereços iguais em outras pastas são apenas sinalizados. Faça backup do Tabby antes de substituir conexões.
|
|
43
|
+
|
|
44
|
+
## Colunas do CSV
|
|
45
|
+
|
|
46
|
+
| group1 | group2 | other_groups | profile | protocol | hostname | port | username |
|
|
47
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
48
|
+
| Região | Cidade | POP / Core | CORE-01 | ssh | 192.0.2.10 | 30022 | netops |
|
|
49
|
+
| Região | Cidade | POP / Acesso | OLT-01 | ssh | 192.0.2.11 | 22 | |
|
|
50
|
+
|
|
51
|
+
Outras colunas: **auth**, **algorithms** e **settings** (JSON seguro). Os dois primeiros níveis de pasta ficam em colunas próprias; níveis mais profundos são codificados em other_groups com separadores escapados. Os IPs da tabela são reservados para exemplos.
|
|
52
|
+
|
|
53
|
+
## Compatibilidade MobaXterm
|
|
54
|
+
|
|
55
|
+
Compatível com bookmarks SSH/Telnet .mxtsessions, nomes, pastas aninhadas, IP/hostname, portas personalizadas, usuários e X11 básico. Configurações avançadas específicas do MobaXterm podem não ser preservadas. Verifique o arquivo no programa de destino antes de usar em produção.
|
|
56
|
+
|
|
57
|
+
## Segurança e limites
|
|
58
|
+
|
|
59
|
+
O processamento ocorre localmente. Hostnames, IPs e usuários autorizados ainda podem ser informações sensíveis. Senhas, conteúdo/caminhos de chaves privadas, scripts e comandos de proxy não são exportados. Substituir um perfil descarta as credenciais específicas anteriores.
|
|
60
|
+
|
|
61
|
+
Limites: **5 MB/arquivo**, **10.000 perfis/arquivo**, **32 níveis de pasta**. O formato do MobaXterm pode rejeitar certos caracteres nos nomes de pastas e sessões.
|
|
62
|
+
|
|
63
|
+
## Documentação
|
|
64
|
+
|
|
65
|
+
- [Guia do Usuário (português)](docs/GUIA_USUARIO.pt-BR.md)
|
|
66
|
+
- [User Guide (English)](docs/USER_GUIDE.md)
|
|
67
|
+
- [Histórico de versões](CHANGELOG.md)
|
|
68
|
+
|
|
69
|
+
## Próximas integrações
|
|
70
|
+
|
|
71
|
+
SecureCRT, mRemoteNG e outros gerenciadores de conexões.
|
|
72
|
+
|
|
73
|
+
MIT © 2026 gugahartmann.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ConfigService, ProfilesService, LocaleService } from 'tabby-core';
|
|
2
|
+
interface Node {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
level: number;
|
|
6
|
+
count: number;
|
|
7
|
+
expanded: boolean;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
partial: boolean;
|
|
10
|
+
children: Node[];
|
|
11
|
+
profiles: any[];
|
|
12
|
+
}
|
|
13
|
+
interface ImportFile {
|
|
14
|
+
name: string;
|
|
15
|
+
mode: 'root' | 'folder';
|
|
16
|
+
folder: string;
|
|
17
|
+
rows: any[];
|
|
18
|
+
preview: any[];
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare class BridgeComponent {
|
|
22
|
+
private profilesService;
|
|
23
|
+
private config;
|
|
24
|
+
private locale;
|
|
25
|
+
tab: 'export' | 'import';
|
|
26
|
+
format: string;
|
|
27
|
+
supportedFormats: any[];
|
|
28
|
+
acceptedExtensions: string;
|
|
29
|
+
profiles: any[];
|
|
30
|
+
groups: any[];
|
|
31
|
+
nodes: Node[];
|
|
32
|
+
chosenIds: Set<string>;
|
|
33
|
+
users: any[];
|
|
34
|
+
sharedUsers: Set<string>;
|
|
35
|
+
search: string;
|
|
36
|
+
exportMode: 'root' | 'folder';
|
|
37
|
+
exportFolder: string;
|
|
38
|
+
files: ImportFile[];
|
|
39
|
+
bulkFrom: string;
|
|
40
|
+
bulkTo: string;
|
|
41
|
+
error: string;
|
|
42
|
+
info: string;
|
|
43
|
+
busy: boolean;
|
|
44
|
+
version: string;
|
|
45
|
+
language: string;
|
|
46
|
+
private localeSubscription;
|
|
47
|
+
constructor(profilesService: ProfilesService, config: ConfigService, locale: LocaleService);
|
|
48
|
+
t(key: string, params?: any): string;
|
|
49
|
+
ngOnDestroy(): void;
|
|
50
|
+
ngOnInit(): Promise<void>;
|
|
51
|
+
reload(): Promise<void>;
|
|
52
|
+
visible(): Node[];
|
|
53
|
+
toggleGroup(n: Node, checked: boolean): void;
|
|
54
|
+
toggleProfile(p: any, checked: boolean): void;
|
|
55
|
+
selectAll(checked: boolean): void;
|
|
56
|
+
refresh(): void;
|
|
57
|
+
get selected(): any[];
|
|
58
|
+
toggleUser(name: string, checked: boolean): void;
|
|
59
|
+
matches(name: string): boolean;
|
|
60
|
+
private download;
|
|
61
|
+
exportFile(): Promise<void>;
|
|
62
|
+
onFiles(event: Event): Promise<void>;
|
|
63
|
+
refreshPreview(file: ImportFile): void;
|
|
64
|
+
removeFile(file: ImportFile): void;
|
|
65
|
+
countMatches(file: ImportFile): number;
|
|
66
|
+
get importCount(): number;
|
|
67
|
+
get importedCount(): number;
|
|
68
|
+
applyBulkUser(): void;
|
|
69
|
+
get importedUsers(): string[];
|
|
70
|
+
setAllActions(action: string): void;
|
|
71
|
+
performImport(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{var e={919(e,t,o){var r=o(784);e.exports=(r.default||r).apply(r,[])},784(e,t,o){o(766),e.exports=function(e){return""+'<div class="p-3"><div class="d-flex justify-content-between align-items-start mb-2"><h2 class="mb-0">Tabby Profile Bridge</h2><span class="small text-muted text-end">by gugahartmann · v{{version}}</span></div><p class="text-muted">{{t(\'description\')}}</p><div class="btn-group mb-3"><button class="btn" [class.btn-primary]="tab==="export"" [class.btn-outline-secondary]="tab!=="export"" (click)="tab="export"">{{t(\'export\')}}</button><button class="btn" [class.btn-primary]="tab==="import"" [class.btn-outline-secondary]="tab!=="import"" (click)="tab="import"">{{t(\'import\')}}</button></div><div class="alert alert-danger" *ngIf="error">{{error}}</div><div class="alert alert-success" *ngIf="info">{{info}}</div><div *ngIf="tab==="export""><div class="d-flex justify-content-between align-items-center mb-2"><h4>{{t(\'selectFolders\')}}</h4><div><button class="btn btn-sm btn-outline-secondary me-2" (click)="selectAll(true)">{{t(\'selectAll\')}}</button><button class="btn btn-sm btn-outline-secondary" (click)="selectAll(false)">{{t(\'clear\')}}</button></div></div><input class="form-control mb-2" type="search" [attr.placeholder]="t(\'searchPlaceholder\')" [(ngModel)]="search"><div class="border rounded p-2 mb-3" style="max-height:320px;overflow-y:auto"><ng-container *ngFor="let node of visible()"><div class="d-flex align-items-center mb-1" *ngIf="matches(node.name)||!search"><button class="btn btn-sm btn-link p-0 me-1" *ngIf="node.children.length" (click)="node.expanded=!node.expanded">{{node.expanded ? \'▾\' : \'▸\'}}</button><span class="me-2" *ngIf="!node.children.length">📁</span><input class="form-check-input me-2" type="checkbox" [checked]="node.selected" [indeterminate]="node.partial" (change)="toggleGroup(node,$any($event.target).checked)"><strong>{{node.name}}</strong><small class="text-muted ms-2">({{node.count}})</small></div><ng-container *ngIf="node.expanded"><div class="d-flex align-items-center mb-1" *ngFor="let profile of node.profiles" [style.paddingLeft.px]="(node.level+1)*20+13"><ng-container *ngIf="matches(profile.name)||matches(profile.options?.host||"")"><input class="form-check-input me-2" type="checkbox" [checked]="chosenIds.has(profile.id)" (change)="toggleProfile(profile,$any($event.target).checked)"><span class="me-2">🖥</span><span>{{profile.name}}</span><small class="text-muted ms-2">{{profile.type}} · {{profile.options?.host}}:{{profile.options?.port}}</small></ng-container></div></ng-container></ng-container></div><h4>{{t(\'exportUsers\')}}</h4><p class="text-muted">{{t(\'userOptIn\')}}</p><div class="border rounded p-2 mb-3" style="max-height:190px;overflow-y:auto"><div class="text-muted" *ngIf="!users.length">{{t(\'selectToListUsers\')}}</div><div class="form-check" *ngFor="let item of users"><input class="form-check-input" type="checkbox" [checked]="sharedUsers.has(item.user)" (change)="toggleUser(item.user,$any($event.target).checked)"><label class="form-check-label">{{item.user}} — {{item.count}} {{t(\'sessions\')}}</label></div></div><h4>{{t(\'exportFolder\')}}</h4><div class="form-check mb-2"><input class="form-check-input" type="radio" name="exportMode" value="root" [(ngModel)]="exportMode" id="exp-root"><label class="form-check-label" for="exp-root">{{t(\'preserveHierarchy\')}}</label></div><div class="form-check mb-2"><input class="form-check-input" type="radio" name="exportMode" value="folder" [(ngModel)]="exportMode" id="exp-folder"><label class="form-check-label" for="exp-folder">{{t(\'wrapExport\')}}</label></div><input class="form-control mb-3" *ngIf="exportMode==="folder"" [(ngModel)]="exportFolder" [attr.placeholder]="t(\'folderPlaceholder\')" maxlength="256"><div class="d-flex gap-2 align-items-center mb-3"><label class="form-label mb-0" for="bridge-format">{{t(\'format\')}}</label><select class="form-select" style="max-width:220px" id="bridge-format" [(ngModel)]="format"><option *ngFor="let adapter of supportedFormats" [value]="adapter.id">{{adapter.label}}</option></select></div><p class="small text-muted" *ngIf="format==="csv"">{{t(\'csvHelp\')}}</p><p class="small text-muted" *ngIf="format==="mxtsessions"">{{t(\'mobaHelp\')}}</p><button class="btn btn-primary" [disabled]="!selected.length||busy" (click)="exportFile()">{{t(\'exportProfiles\', {count: selected.length})}}</button></div><div *ngIf="tab==="import""><h4>{{t(\'addFiles\')}}</h4><p class="text-muted">{{t(\'importDescription\')}}</p><input class="form-control mb-3" type="file" multiple [attr.accept]="acceptedExtensions" (change)="onFiles($event)"><div class="card mb-3" *ngFor="let file of files"><div class="card-body"><div class="d-flex justify-content-between"><h5>{{file.name}} — {{file.preview.length}} {{t(\'sessions\')}}</h5><button class="btn btn-sm btn-outline-danger" (click)="removeFile(file)">{{t(\'remove\')}}</button></div><div class="form-check"><input class="form-check-input" type="radio" [name]=""mode-"+file.name" value="root" [(ngModel)]="file.mode" (change)="refreshPreview(file)"><label class="form-check-label">{{t(\'importRoot\')}}</label></div><div class="form-check"><input class="form-check-input" type="radio" [name]=""mode-"+file.name" value="folder" [(ngModel)]="file.mode" (change)="refreshPreview(file)"><label class="form-check-label">{{t(\'importFolder\')}}</label></div><input class="form-control mt-2" *ngIf="file.mode==="folder"" [(ngModel)]="file.folder" (ngModelChange)="refreshPreview(file)" maxlength="256" [attr.placeholder]="t(\'parentFolderPlaceholder\')"><div class="small text-muted mt-2">{{t(\'matchingNames\', {count:countMatches(file)})}}</div><div class="alert alert-warning mt-2" *ngIf="file.warnings.length">{{t(\'mobaWarning\', {count:file.warnings.length, first:file.warnings[0]})}}</div></div></div><div *ngIf="files.length"><h4>{{t(\'bulkUser\')}}</h4><div class="row g-2 mb-3"><div class="col-md-4"><label class="form-label">{{t(\'originalUser\')}}</label><select class="form-select" [(ngModel)]="bulkFrom"><option value="*">{{t(\'allUsers\')}}</option><option *ngFor="let name of importedUsers" [value]="name">{{name || t(\'emptyUser\')}}</option></select></div><div class="col-md-4"><label class="form-label">{{t(\'newUser\')}}</label><input class="form-control" [(ngModel)]="bulkTo" maxlength="255" [attr.placeholder]="t(\'newUserPlaceholder\')"></div><div class="col-md-4 d-flex align-items-end"><button class="btn btn-outline-primary w-100" (click)="applyBulkUser()">{{t(\'applyUser\')}}</button></div></div><div class="d-flex justify-content-between align-items-center mb-2"><h4>{{t(\'reviewProfiles\', {count:importedCount})}}</h4><div class="btn-group"><button class="btn btn-sm btn-outline-secondary" (click)="setAllActions("skip")">{{t(\'skipAll\')}}</button><button class="btn btn-sm btn-outline-secondary" (click)="setAllActions("create")">{{t(\'createAll\')}}</button></div></div><div class="border rounded mb-3" style="max-height:370px;overflow:auto"><table class="table table-sm table-striped mb-0"><thead><tr><th>{{t(\'session\')}}</th><th>{{t(\'hostPort\')}}</th><th>{{t(\'username\')}}</th><th>{{t(\'match\')}}</th><th>{{t(\'action\')}}</th></tr></thead><tbody><ng-container *ngFor="let file of files"><tr *ngFor="let row of file.preview"><td><strong>{{row.name}}</strong><div class="small text-muted">{{row.path.join(\' / \') || t(\'root\')}}</div></td><td><span>{{row.options.host}}:{{row.options.port}}</span><div class="small text-muted">{{row.type}}</div></td><td><input class="form-control form-control-sm" [ngModel]="row.editedUser===undefined?row.options.user:row.editedUser" (ngModelChange)="row.editedUser=$event" maxlength="255"></td><td><span class="text-warning" *ngIf="row.matchedId">{{t(\'sameName\')}}</span><span class="text-muted" *ngIf="!row.matchedId&&row.sameEndpointId">{{t(\'sameEndpoint\')}}</span><span class="text-muted" *ngIf="!row.matchedId&&!row.sameEndpointId">{{t(\'new\')}}</span></td><td><select class="form-select form-select-sm" [(ngModel)]="row.action"><option value="skip">{{t(\'skip\')}}</option><option value="create">{{t(\'create\')}}</option><option *ngIf="row.matchedId" value="replace">{{t(\'replace\')}}</option></select></td></tr></ng-container></tbody></table></div><p class="small text-muted">{{t(\'replaceWarning\')}}</p><button class="btn btn-primary" [disabled]="!importCount||busy" (click)="performImport()">{{busy ? t(\'importing\') : t(\'applyChanges\', {count:importCount})}}</button></div></div></div>'}},766(e,t,o){"use strict";var r=Object.prototype.hasOwnProperty;function s(e,t){return Array.isArray(e)?function(e,t){for(var o,r="",n="",i=Array.isArray(t),a=0;a<e.length;a++)(o=s(e[a]))&&(i&&t[a]&&(o=l(o)),r=r+n+o,n=" ");return r}(e,t):e&&"object"==typeof e?function(e){var t="",o="";for(var s in e)s&&e[s]&&r.call(e,s)&&(t=t+o+s,o=" ");return t}(e):e||""}function n(e){if(!e)return"";if("object"==typeof e){var t="";for(var o in e)r.call(e,o)&&(t=t+o+":"+e[o]+";");return t}return e+""}function i(e,t,o,r){if(!1===t||null==t||!t&&("class"===e||"style"===e))return"";if(!0===t)return" "+(r?e:e+'="'+e+'"');var s=typeof t;return"object"!==s&&"function"!==s||"function"!=typeof t.toJSON||(t=t.toJSON()),"string"==typeof t||(t=JSON.stringify(t),o||-1===t.indexOf('"'))?(o&&(t=l(t))," "+e+'="'+t+'"'):" "+e+"='"+t.replace(/'/g,"'")+"'"}t.merge=function e(t,o){if(1===arguments.length){for(var r=t[0],s=1;s<t.length;s++)r=e(r,t[s]);return r}for(var i in o)if("class"===i){var a=t[i]||[];t[i]=(Array.isArray(a)?a:[a]).concat(o[i]||[])}else if("style"===i){a=(a=n(t[i]))&&";"!==a[a.length-1]?a+";":a;var l=n(o[i]);l=l&&";"!==l[l.length-1]?l+";":l,t[i]=a+l}else t[i]=o[i];return t},t.classes=s,t.style=n,t.attr=i,t.attrs=function(e,t){var o="";for(var a in e)if(r.call(e,a)){var l=e[a];if("class"===a){o=i(a,l=s(l),!1,t)+o;continue}"style"===a&&(l=n(l)),o+=i(a,l,!1,t)}return o};var a=/["&<>]/;function l(e){var t=""+e,o=a.exec(t);if(!o)return e;var r,s,n,i="";for(r=o.index,s=0;r<t.length;r++){switch(t.charCodeAt(r)){case 34:n=""";break;case 38:n="&";break;case 60:n="<";break;case 62:n=">";break;default:continue}s!==r&&(i+=t.substring(s,r)),s=r+1,i+=n}return s!==r?i+t.substring(s,r):i}t.escape=l,t.rethrow=function e(t,r,s,n){if(!(t instanceof Error))throw t;if(!("undefined"==typeof window&&r||n))throw t.message+=" on line "+s,t;var i,a,l,c;try{n=n||o(21).readFileSync(r,{encoding:"utf8"}),i=3,a=n.split("\n"),l=Math.max(s-i,0),c=Math.min(a.length,s+i)}catch(o){return t.message+=" - could not read from "+r+" ("+o.message+")",void e(t,null,s)}i=a.slice(l,c).map(function(e,t){var o=t+l+1;return(o==s?" > ":" ")+o+"| "+e}).join("\n"),t.path=r;try{t.message=(r||"Pug")+":"+s+"\n"+i+"\n\n"+t.message}catch(e){}throw t}},353(e,t,o){"use strict";var r=this&&this.__decorate||function(e,t,o,r){var s,n=arguments.length,i=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,o,r);else for(var a=e.length-1;a>=0;a--)(s=e[a])&&(i=(n<3?s(i):n>3?s(t,o,i):s(t,o))||i);return n>3&&i&&Object.defineProperty(t,o,i),i},s=this&&this.__metadata||function(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)};Object.defineProperty(t,"__esModule",{value:!0}),t.BridgeComponent=void 0;const n=o(430),i=o(368),a=o(56),l=o(979),c=o(646);let p=class{constructor(e,t,r){this.profilesService=e,this.config=t,this.locale=r,this.tab="export",this.format="csv",this.supportedFormats=l.FORMATS,this.acceptedExtensions=l.FORMATS.map(e=>e.extension).join(","),this.profiles=[],this.groups=[],this.nodes=[],this.chosenIds=new Set,this.users=[],this.sharedUsers=new Set,this.search="",this.exportMode="root",this.exportFolder="Tabby Export",this.files=[],this.bulkFrom="*",this.bulkTo="",this.error="",this.info="",this.busy=!1,this.version=o(330).rE,this.language="en-US",this.language=this.locale.getLocale(),this.localeSubscription=this.locale.localeChanged$.subscribe(e=>{this.language=e})}t(e,t={}){return c.translate(this.language,e,t)}ngOnDestroy(){this.localeSubscription&&this.localeSubscription.unsubscribe()}async ngOnInit(){await this.reload()}async reload(){this.error="";const e=await this.profilesService.getProfiles({includeBuiltin:!1,clone:!0});this.profiles=e.filter(e=>!("ssh"!==e.type&&"telnet"!==e.type||e.isTemplate||e.isBuiltin)).map(e=>{const t=this.profilesService.getConfigProxyForProfile(e);return{...e,options:{...t.options}}}),this.groups=this.profilesService.getSyncProfileGroups();const t=new Set(this.groups.map(e=>e.id)),o=new Map;for(const e of this.groups){const r=t.has(e.parentGroupId)?e.parentGroupId:"";o.set(r,[...o.get(r)||[],e])}const r=(e,t,s)=>(o.get(e)||[]).filter(e=>!s.has(e.id)).map(e=>{const o=r(e.id,t+1,new Set([...s,e.id])),n=this.profiles.filter(t=>t.group===e.id);return{id:e.id,name:e.name,level:t,count:n.length+o.reduce((e,t)=>e+t.count,0),expanded:t<2,selected:!1,partial:!1,children:o,profiles:n}});this.nodes=r("",0,new Set);const s=this.profiles.filter(e=>!e.group||!t.has(e.group));s.length&&this.nodes.push({id:"__root__",name:this.t("ungrouped"),level:0,count:s.length,expanded:!0,selected:!1,partial:!1,children:[],profiles:s}),this.chosenIds.clear(),this.sharedUsers.clear(),this.refresh()}visible(){const e=[],t=o=>{e.push(o),o.expanded&&o.children.forEach(t)};return this.nodes.forEach(t),e}toggleGroup(e,t){const o=e=>{e.profiles.forEach(e=>t?this.chosenIds.add(e.id):this.chosenIds.delete(e.id)),e.children.forEach(o)};o(e),this.refresh()}toggleProfile(e,t){t?this.chosenIds.add(e.id):this.chosenIds.delete(e.id),this.refresh()}selectAll(e){this.chosenIds=new Set(e?this.profiles.map(e=>e.id):[]),this.refresh()}refresh(){const e=t=>{t.children.forEach(e);const o=t.profiles.filter(e=>this.chosenIds.has(e.id)).length;t.selected=o===t.profiles.length&&t.children.every(e=>e.selected)&&t.profiles.length+t.children.length>0,t.partial=!t.selected&&(o>0||t.children.some(e=>e.selected||e.partial))};this.nodes.forEach(e),this.users=a.usersOf(this.selected);const t=new Set(this.users.map(e=>e.user));for(const e of this.sharedUsers)t.has(e)||this.sharedUsers.delete(e)}get selected(){return this.profiles.filter(e=>this.chosenIds.has(e.id))}toggleUser(e,t){t?this.sharedUsers.add(e):this.sharedUsers.delete(e)}matches(e){return!this.search||e.toLowerCase().includes(this.search.toLowerCase())}download(e,t,o){const r=new Blob([t],{type:o}),s=URL.createObjectURL(r),n=document.createElement("a");n.href=s,n.download=e,document.body.appendChild(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(s),1e3)}async exportFile(){this.error="",this.info="";try{const e="folder"===this.exportMode?this.exportFolder.trim():"";if(e&&(e.includes("/")||e.includes("\\")))throw Error(this.t("invalidFolder"));const t=a.rowsForExport(this.profiles,this.groups,this.chosenIds,this.sharedUsers,e),o=l.getFormat(this.format),r=o.encode(t);this.download("tabby-profile-bridge"+o.extension,r,o.mimeType),this.info=this.t("exportedSuccess",{count:t.length})}catch(e){this.error=String(e.message||e)}}async onFiles(e){this.error="",this.info="";const t=e.target,o=Array.from(t.files||[]);try{for(const e of o){if(e.size>a.MAX_BYTES)throw Error(this.t("tooLarge",{file:e.name}));const t=l.detectFormat(e.name),o=t.binary?new Uint8Array(await e.arrayBuffer()):await e.text(),r=t.decode(o),s=r.rows,n={name:e.name,mode:"root",folder:e.name.replace(/\.[^.]+$/,""),rows:s,preview:[],warnings:r.warnings};this.files.push(n),this.refreshPreview(n)}}catch(e){this.error=String(e.message||e)}t.value=""}refreshPreview(e){e.preview=a.makePreview(e.rows,this.profiles,this.groups,"folder"===e.mode?e.folder.trim():"")}removeFile(e){this.files=this.files.filter(t=>t!==e)}countMatches(e){return e.preview.filter(e=>Boolean(e.matchedId)).length}get importCount(){return this.files.reduce((e,t)=>e+t.preview.filter(e=>"skip"!==e.action).length,0)}get importedCount(){return this.files.reduce((e,t)=>e+t.preview.length,0)}applyBulkUser(){if(this.error="",this.bulkTo.length>255)this.error=this.t("tooLongUser");else for(const e of this.files)for(const t of e.preview)"skip"===t.action||"*"!==this.bulkFrom&&t.options.user!==this.bulkFrom||(t.editedUser=this.bulkTo)}get importedUsers(){const e=new Set;return this.files.forEach(t=>t.preview.forEach(t=>e.add(t.options.user||""))),[...e].sort()}setAllActions(e){for(const t of this.files)for(const o of t.preview)o.action="replace"!==e||o.matchedId?e:"create"}async performImport(){if(!this.busy){this.error="",this.info="",this.busy=!0;try{const e=this.files.flatMap(e=>e.preview.filter(e=>"skip"!==e.action));if(!e.length)throw Error(this.t("selectImport"));const t=()=>Math.random().toString(36).slice(2)+Date.now().toString(36),o=a.buildPlan(e,this.groups,t),r=[...this.config.store.profiles],s=[...this.config.store.groups];try{const e=new Map(o.replaces.map(e=>[e.id,e]));this.config.store.profiles=this.config.store.profiles.map(t=>e.get(t.id)||t),this.config.store.profiles.push(...o.creates),this.config.store.groups.push(...o.groups),await this.config.save()}catch(e){this.config.store.profiles=r,this.config.store.groups=s;try{await this.config.save()}catch(e){}throw e}this.info=this.t("importedSuccess",{created:o.creates.length,replaced:o.replaces.length,groups:o.groups.length}),this.files=[],await this.reload()}catch(e){this.error=String(e.message||e)}finally{this.busy=!1}}}};p=r([(0,n.Component)({template:o(919)}),s("design:paramtypes",[i.ProfilesService,i.ConfigService,i.LocaleService])],p),t.BridgeComponent=p},729(e,t,o){"use strict";var r=this&&this.__decorate||function(e,t,o,r){var s,n=arguments.length,i=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,o,r);else for(var a=e.length-1;a>=0;a--)(s=e[a])&&(i=(n<3?s(i):n>3?s(t,o,i):s(t,o))||i);return n>3&&i&&Object.defineProperty(t,o,i),i};Object.defineProperty(t,"__esModule",{value:!0});const s=o(430),n=o(804),i=o(440),a=o(643),l=o(262),c=o(977),p=o(353);let d=class{};d=r([(0,s.NgModule)({imports:[n.CommonModule,i.FormsModule,a.NgbModule],declarations:[p.BridgeComponent],providers:[{provide:l.SettingsTabProvider,useClass:c.BridgeSettingsProvider,multi:!0}]})],d),t.default=d},977(e,t,o){"use strict";var r=this&&this.__decorate||function(e,t,o,r){var s,n=arguments.length,i=n<3?t:null===r?r=Object.getOwnPropertyDescriptor(t,o):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,o,r);else for(var a=e.length-1;a>=0;a--)(s=e[a])&&(i=(n<3?s(i):n>3?s(t,o,i):s(t,o))||i);return n>3&&i&&Object.defineProperty(t,o,i),i};Object.defineProperty(t,"__esModule",{value:!0}),t.BridgeSettingsProvider=void 0;const s=o(430),n=o(262),i=o(353);let a=class extends n.SettingsTabProvider{constructor(){super(...arguments),this.id="profile-bridge",this.title="Profile Bridge",this.icon="exchange-alt",this.weight=210}getComponentType(){return i.BridgeComponent}};a=r([(0,s.Injectable)()],a),t.BridgeSettingsProvider=a},56(e){"use strict";const t=["group1","group2","other_groups","profile","protocol","hostname","port","username","auth","algorithms","settings"],o=1e4,r=5242880,s=["hmac","kex","cipher","serverHostKey","compression"],n=["x11","skipBanner","agentForward","keepaliveInterval","keepaliveCountMax","readyTimeout","term","reuseSession","warnOnClose"],i=new Set(["ssh","telnet"]);function a(e){throw new Error(e)}function l(e,t){e||a(t)}function c(e,t=512){return"string"==typeof e&&e.length<=t}function p(e){return!!e&&"object"==typeof e&&!Array.isArray(e)&&Object.getPrototypeOf(e)===Object.prototype}function d(e){return e.startsWith("'")&&/^[=+@\- \t\r\n]*[=+@\-]/.test(e.slice(1))?e.slice(1):e}function u(e){if(!e)return[];const t=[];let o="",r=!1;for(const s of e)r?(o+=s,r=!1):"\\"===s?r=!0:"/"===s?(t.push(o.trim()),o=""):o+=s;return r&&(o+="\\"),t.push(o.trim()),l(t.every(Boolean),"Empty intermediate folder"),t}function h(e){return e.map(e=>e.replace(/\\/g,"\\\\").replace(/\//g,"\\/")).join(" / ")}function m(e,t){const o=(r=e.protocol,String(r||"ssh").toLowerCase().trim());var r;l(i.has(o),"Row "+t+": unsupported protocol"),l(c(e.profile)&&!!e.profile.trim(),"Row "+t+": missing profile name"),l(c(e.hostname,255)&&!!e.hostname.trim(),"Row "+t+": missing hostname"),l(c(e.username||"",255),"Row "+t+": invalid username"),l(/^\d{1,5}$/.test(String(e.port||""))&&+e.port>=1&&+e.port<=65535,"Row "+t+": invalid port");const d=[e.group1,e.group2,...u(e.other_groups)].filter(Boolean);l(d.length<=32&&d.every(e=>c(e,256)),"Row "+t+": invalid folder depth/name");let h={};if(e.algorithms){try{h=JSON.parse(e.algorithms)}catch{a("Row "+t+": invalid algorithms JSON")}l(p(h),"Row "+t+": invalid algorithms object");for(const[e,o]of Object.entries(h))l(s.includes(e)&&Array.isArray(o)&&o.length<=64&&o.every(e=>c(e,128)&&/^[a-zA-Z0-9_.@,+-]+$/.test(e)),"Row "+t+": invalid SSH algorithms")}let m={};if(e.settings){try{m=JSON.parse(e.settings)}catch{a("Row "+t+": invalid settings JSON")}l(p(m),"Row "+t+": invalid settings");for(const[e,o]of Object.entries(m))l(n.includes(e),"Row "+t+": unsupported setting "+e),["term"].includes(e)?l(c(o,80),"Invalid terminal"):["keepaliveInterval","keepaliveCountMax","readyTimeout"].includes(e)?l(null===o||Number.isInteger(o)&&o>=0&&o<=2147483647,"Invalid timeout"):l(null===o||"boolean"==typeof o,"Invalid boolean option")}return l(["","password","publicKey","agent","keyboardInteractive"].includes(e.auth||""),"Row "+t+": unsupported authentication"),{path:d,name:e.profile,type:o,options:{host:e.hostname,port:Number(e.port),user:e.username||"",...e.auth?{auth:e.auth}:{},...Object.keys(h).length?{algorithms:h}:{},...m}}}function f(e,t){const o=new Map(t.map(e=>[e.id,e])),r=[],s=new Set;for(;e&&o.has(e);){l(!s.has(e),"Circular group detected"),s.add(e);const t=o.get(e);l(c(t.name,256),"Invalid group name"),r.unshift(t.name),e=t.parentGroupId,l(r.length<=32,"Group depth exceeds 32 levels")}return r}function g(e,t){return[e.type,...t,e.name].map(e=>String(e||"").toLowerCase()).join("")}function v(e){const t=e.options||{};return[e.type,t.host||"",t.port||("ssh"===e.type?22:23),t.user||""].map(e=>String(e).toLowerCase()).join("")}e.exports={MAX_BYTES:r,MAX_ROWS:o,readCSV:function(e){const t=function(e){l(c(e,r),"CSV file is too large"),e=e.replace(/^\uFEFF/,"");const t=[];let o=[],s="",n=!1,i=!1;for(let r=0;r<e.length;r++){const a=e[r];n?'"'===a&&'"'===e[r+1]?(s+='"',r++):'"'===a?(n=!1,i=!0):s+=a:'"'!==a||s||i?","===a?(o.push(d(s)),s="",i=!1):"\n"===a||"\r"===a?("\r"===a&&"\n"===e[r+1]&&r++,o.push(d(s)),o.some(e=>""!==e)&&t.push(o),o=[],s="",i=!1):(l(!i||/\s/.test(a),"Unexpected text after CSV quote"),s+=a):n=!0,l(s.length<=1e4,"CSV cell is too long")}return l(!n,"Unclosed CSV quote"),(s||o.length)&&(o.push(d(s)),t.push(o)),t}(e);l(t.length>=1,"CSV file is empty");const s=t.shift().map(e=>e.trim().toLowerCase()),n={name:"profile",host:"hostname",user:"username",folder1:"group1",folder2:"group2",group3:"other_groups"},i=s.map(e=>n[e]||e);l(new Set(i).size===i.length,"Duplicate CSV headers");for(const e of["profile","hostname","port"])l(i.includes(e),"Required CSV column missing: "+e);return l(t.length<=o,"Too many sessions in CSV"),t.map((e,t)=>{l(e.length<=i.length,"Row "+(t+2)+": unexpected columns");const o={};return i.forEach((t,r)=>o[t]=e[r]||""),m(o,t+2)})},stringifyCSV:function(e){return"\ufeff"+[t.join(","),...e.map(e=>t.map(t=>{return o=function(e){return e=String(e??""),/^[=+@\- \t\r\n]*[=+@\-]/.test(e)?"'"+e:e}(o=e[t]),/[",\r\n]/.test(o)?'"'+o.replace(/"/g,'""')+'"':o;var o}).join(","))].join("\r\n")+"\r\n"},rowsForExport:function(e,t,r,a,d=""){const u=e.filter(e=>r.has(e.id)&&i.has(e.type));l(u.length&&u.length<=o,"Select 1 to 10000 connections");const g=u.map(e=>{const o=e.options||{};l(c(e.name)&&!!e.name.trim(),"Invalid name"),l(c(o.host,255)&&!!o.host.trim(),"Missing hostname: "+e.name);const r=[...d?[d]:[],...f(e.group,t)],i=function(e){const t=e.options||{},o={};if(p(t.algorithms))for(const e of s){const r=t.algorithms[e];Array.isArray(r)&&r.length<=64&&r.every(e=>c(e,128)&&/^[a-zA-Z0-9_.@,+-]+$/.test(e))&&r.length&&(o[e]=r)}const r={};for(const e of n){const o=t[e];void 0!==o&&("term"===e&&c(o,80)||["keepaliveInterval","keepaliveCountMax","readyTimeout"].includes(e)&&(null===o||Number.isInteger(o)&&o>=0&&o<=2147483647)?r[e]=o:null!==o&&"boolean"!=typeof o||(r[e]=o))}return{algorithms:o,settings:r}}(e);return{group1:r[0]||"",group2:r[1]||"",other_groups:h(r.slice(2)),profile:e.name,protocol:e.type,hostname:o.host,port:String(o.port||("ssh"===e.type?22:23)),username:a.has(o.user)?o.user:"",auth:["password","publicKey","agent","keyboardInteractive"].includes(o.auth)?o.auth:"",algorithms:Object.keys(i.algorithms).length?JSON.stringify(i.algorithms):"",settings:Object.keys(i.settings).length?JSON.stringify(i.settings):""}});return g.forEach((e,t)=>m(e,t+2)),g},usersOf:function(e){const t=new Map;return e.forEach(e=>{const o=e.options?.user;o&&t.set(o,(t.get(o)||0)+1)}),[...t].sort((e,t)=>e[0].localeCompare(t[0])).map(([e,t])=>({user:e,count:t}))},makePreview:function(e,t,o,r=""){return l(c(r,256),"Invalid wrapper folder"),e.map((e,s)=>{const n=[...r?[r]:[],...e.path],i=t.find(t=>g(t,f(t.group,o))===g(e,n)),a=t.find(t=>v(t)===v(e));return{...e,index:s,path:n,matchedId:i?.id||"",sameEndpointId:a?.id||"",action:i?"skip":"create"}})},buildPlan:function(e,t,o){l(Array.isArray(e)&&"function"==typeof o,"Invalid plan");const r=t.map(e=>({...e})),s=[],n=[],i=[],a=new Set;function p(e,t){return r.find(o=>o.name===e&&(o.parentGroupId||"")===(t||""))}for(const t of e){if("skip"===t.action)continue;l("create"===t.action||"replace"===t.action,"Invalid action"),l(!a.has(t.index),"Duplicate import row"),a.add(t.index);let e="";for(const n of t.path){let t=p(n,e);t||(t={id:o(),name:n,...e?{parentGroupId:e}:{}},r.push(t),s.push(t)),e=t.id}const d={type:t.type,name:t.name,group:e,options:{...t.options,...void 0!==t.editedUser?{user:t.editedUser}:{}}};l(c(d.options.user||"",255),"Invalid edited username"),"replace"===t.action?(l(t.matchedId,"Cannot replace without an exact match"),i.push({...d,id:t.matchedId})):n.push({...d,id:t.type+":custom:bridge:"+o()})}return{groups:s,creates:n,replaces:i}},validateRow:m,joinOther:h}},979(e,t,o){"use strict";const r=o(56),s=o(684),n=Object.freeze([{id:"csv",label:"CSV (.csv)",extension:".csv",mimeType:"text/csv;charset=utf-8",binary:!1,encode:e=>r.stringifyCSV(e),decode:e=>({rows:r.readCSV(e),warnings:[]})},{id:"mxtsessions",label:"MobaXterm (.mxtsessions)",extension:".mxtsessions",mimeType:"application/octet-stream",binary:!0,encode:e=>s.writeMoba(e),decode:e=>s.readMoba(e)}]);e.exports={FORMATS:n,getFormat:function(e){const t=n.find(t=>t.id===e);if(!t)throw new Error("Unsupported import/export format: "+e);return t},detectFormat:function(e){const t=String(e||"").toLowerCase(),o=n.find(e=>t.endsWith(e.extension));if(!o)throw new Error("Unsupported file format: "+e);return o}}},646(e){"use strict";const t={description:{en:"Import, export, and convert SSH/Telnet profiles between terminal clients. Passwords, login scripts, private keys, and local key paths are never exported.",pt:"Importe, exporte e converta perfis SSH/Telnet entre clientes de terminal. Senhas, scripts de login, chaves privadas e caminhos locais de chaves nunca são exportados."},export:{en:"Export",pt:"Exportar"},import:{en:"Import",pt:"Importar"},selectFolders:{en:"Select folders and sessions",pt:"Selecione pastas e sessões"},selectAll:{en:"Select all",pt:"Selecionar tudo"},clear:{en:"Clear",pt:"Limpar"},searchPlaceholder:{en:"Filter by name (selection is preserved)",pt:"Filtrar por nome (a seleção é preservada)"},ungrouped:{en:"Ungrouped",pt:"Sem grupo"},exportUsers:{en:"Export usernames",pt:"Exportar usuários"},userOptIn:{en:"Usernames are excluded by default. Explicitly select the shared service accounts to include.",pt:"Usuários são omitidos por padrão. Selecione explicitamente as contas compartilhadas que deseja incluir."},selectToListUsers:{en:"Select sessions to see their usernames.",pt:"Selecione sessões para listar os usuários."},sessions:{en:"sessions",pt:"sessões"},exportFolder:{en:"Export destination folder",pt:"Pasta de destino da exportação"},preserveHierarchy:{en:"Preserve hierarchy at root",pt:"Preservar hierarquia na raiz"},wrapExport:{en:"Place selected sessions inside one top-level folder",pt:"Colocar as sessões selecionadas dentro de uma pasta principal"},folderPlaceholder:{en:"Top-level folder name",pt:"Nome da pasta principal"},format:{en:"Format",pt:"Formato"},csvHelp:{en:"Algorithms and safe terminal options are stored as JSON strings in CSV cells. Group colors and icons are not exported.",pt:"Algoritmos e opções seguras de terminal são armazenados como texto JSON nas células do CSV. Cores e ícones dos grupos não são exportados."},mobaHelp:{en:"MobaXterm export preserves names, hierarchy, hosts, ports, usernames, and basic X11. Unsupported advanced options, colors, icons, and secrets are omitted.",pt:"A exportação MobaXterm preserva nomes, hierarquia, hosts, portas, usuários e X11 básico. Opções avançadas incompatíveis, cores, ícones e segredos são omitidos."},exportProfiles:{en:"Export {count} profiles",pt:"Exportar {count} perfis"},addFiles:{en:"Add CSV or MobaXterm files",pt:"Adicionar arquivos CSV ou MobaXterm"},importDescription:{en:"Import each file at the root or inside its own parent folder. Review conflicts before applying; existing profiles are never replaced automatically.",pt:"Importe cada arquivo na raiz ou dentro de sua própria pasta principal. Revise os conflitos antes de aplicar; perfis existentes nunca são substituídos automaticamente."},remove:{en:"Remove",pt:"Remover"},importRoot:{en:"Import at Tabby root (keep source hierarchy)",pt:"Importar na raiz do Tabby (manter hierarquia de origem)"},importFolder:{en:"Import inside one parent folder",pt:"Importar dentro de uma pasta principal"},parentFolderPlaceholder:{en:"Parent folder name",pt:"Nome da pasta principal"},matchingNames:{en:"{count} matching names in destination",pt:"{count} nomes coincidentes no destino"},mobaWarning:{en:"{count} MobaXterm entries/settings were not converted. First: {first}",pt:"{count} entradas/configurações do MobaXterm não foram convertidas. Primeira: {first}"},bulkUser:{en:"Bulk username conversion",pt:"Alteração de usuário em massa"},originalUser:{en:"Original username",pt:"Usuário original"},allUsers:{en:"All imported usernames",pt:"Todos os usuários importados"},emptyUser:{en:"(empty)",pt:"(vazio)"},newUser:{en:"New username",pt:"Novo usuário"},newUserPlaceholder:{en:"New SSH username",pt:"Novo usuário SSH"},applyUser:{en:"Apply to selected imports",pt:"Aplicar às importações selecionadas"},reviewProfiles:{en:"Review {count} imported profiles",pt:"Revisar {count} perfis importados"},skipAll:{en:"Skip all",pt:"Ignorar todos"},createAll:{en:"Create all",pt:"Criar todos"},session:{en:"Session",pt:"Sessão"},hostPort:{en:"Host : port",pt:"Host : porta"},username:{en:"Username",pt:"Usuário"},match:{en:"Existing match",pt:"Correspondência existente"},action:{en:"Action",pt:"Ação"},root:{en:"(root)",pt:"(raiz)"},sameName:{en:"Same name / folder",pt:"Mesmo nome / pasta"},sameEndpoint:{en:"Same endpoint elsewhere",pt:"Mesmo destino em outra pasta"},new:{en:"New",pt:"Novo"},skip:{en:"Keep / skip",pt:"Manter / ignorar"},create:{en:"Add as new",pt:"Adicionar como novo"},replace:{en:"Replace existing",pt:"Substituir existente"},replaceWarning:{en:"Replacing a profile removes its connection-specific credentials. Back up your Tabby configuration first. Imported files are never executed.",pt:"Substituir um perfil remove suas credenciais específicas. Faça backup da configuração do Tabby antes. Arquivos importados nunca são executados."},applyChanges:{en:"Apply {count} selected changes",pt:"Aplicar {count} alterações selecionadas"},importing:{en:"Importing...",pt:"Importando..."},exportedSuccess:{en:"Exported {count} sessions without passwords or private keys.",pt:"Exportadas {count} sessões sem senhas ou chaves privadas."},importedSuccess:{en:"Imported {created} new sessions, replaced {replaced}, created {groups} folders.",pt:"Importadas {created} sessões novas, substituídas {replaced} e criadas {groups} pastas."},invalidFolder:{en:"Container folder cannot contain slashes.",pt:"O nome da pasta principal não pode conter barras."},tooLarge:{en:"File exceeds 5 MB: {file}",pt:"O arquivo excede 5 MB: {file}"},tooLongUser:{en:"Username is too long.",pt:"O nome de usuário é muito longo."},selectImport:{en:"Select at least one profile to import.",pt:"Selecione pelo menos um perfil para importar."},errorPrefix:{en:"Operation failed: {error}",pt:"Falha na operação: {error}"}};e.exports={translate:function(e,o,r={}){const s=t[o];return(s?s[function(e){return"string"==typeof e&&/^pt(?:-|$)/i.test(e)?"pt":"en"}(e)]:o).replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g,(e,t)=>Object.prototype.hasOwnProperty.call(r,t)?String(r[t]):e)}}},684(e,t,o){"use strict";const r=o(56),s="0%localhost%22%%%-1%-1%%%%%0%0%0%%%-1%0%0%0%%1080%%0%0%1%%0%%%%0%-1%-1%0".split("%"),n="MobaFont%10%0%0%-1%15%236,236,236%30,30,30%180,180,192%0%-1%0%%xterm%-1%0%_Std_Colors_0_%80%24%0%0%-1%<none>%%0%0%-1%-1".split("%"),i=new Map([[8364,128],[8218,130],[402,131],[8222,132],[8230,133],[8224,134],[8225,135],[710,136],[8240,137],[352,138],[8249,139],[338,140],[381,142],[8216,145],[8217,146],[8220,147],[8221,148],[8226,149],[8211,150],[8212,151],[732,152],[8482,153],[353,154],[8250,155],[339,156],[382,158],[376,159]]);function a(e,t){if("string"!=typeof e||!e.trim()||/[\r\n=#%\\]/.test(e))throw Error("Invalid MobaXterm "+t+": rename to remove newlines, =, #, %, or backslash");return e}function l(e){if(!Array.isArray(e)||e.length>32)throw Error("Invalid MobaXterm group depth");for(const t of e)a(t,"folder");return e.join("\\")}function c(e){if(a(e.name,"session"),/[%#\r\n]/.test(e.options.host)||/[%#\r\n]/.test(e.options.user||""))throw Error("Unsupported MobaXterm delimiter in host/username");const t=e.options;if("telnet"===e.type)return e.name+"=#98#1%"+t.host+"%"+t.port+"%"+(t.user||"")+"%";if("ssh"!==e.type)throw Error("Unsupported MobaXterm protocol");const o=[...s],r=[...n];return o[1]=t.host,o[2]=String(t.port),o[3]=t.user||"",o[5]=t.x11?"-1":"0",o[12]=o[3]?"0":"-1",o[14]="",o[7]="",o[8]="",o[9]="",o[10]="",o[15]="",o[19]="0",o[20]="",o[22]="",o[26]="",void 0!==t.agentForward&&(o[34]=t.agentForward?"-1":"0"),["xterm","xterm-256color","vt100","vt220","vt400","xterm-r6"].includes(t.term)&&(r[13]=t.term),e.name+"=#109#"+o.join("%")+"#"+r.join("%")+"#0# #-1"}e.exports={readMoba:function(e){const t="string"==typeof e?e:(o=e,new TextDecoder("windows-1252",{fatal:!1}).decode(o));var o;if(t.length>r.MAX_BYTES)throw Error("MobaXterm file exceeds 5 MB");const s=[],n=[];let i=[],a=!1,l=!1;for(const e of t.replace(/^\uFEFF/,"").split(/\r?\n/)){const t=e.trim();if(!t||t.startsWith(";")||t.startsWith("#"))continue;if(/^\[/.test(t)){a=/^\[Bookmarks(?:_\d+)?\]$/i.test(t),a&&(l=!0,i=[]);continue}if(!a)continue;if(/^SubRep=/i.test(t)){const e=t.slice(t.indexOf("=")+1);if(i=e?e.split("\\"):[],i.length>32)throw Error("MobaXterm has more than 32 group levels");continue}if(/^(ImgNum|Expanded)=/i.test(t))continue;const o=t.indexOf("=");if(o<=0)continue;const c=t.slice(0,o).trim(),p=t.slice(o+1).trim(),d=/#(109|98)#([01])%/.exec(p);if(!d){n.push("Unsupported session type ignored: "+c);continue}const u="109"===d[1]&&"0"===d[2]?"ssh":"98"===d[1]&&"1"===d[2]?"telnet":null;if(!u){n.push("Unsupported session type ignored: "+c);continue}const h=p.slice(d.index).split("#")[2].split("%"),m={group1:i[0]||"",group2:i[1]||"",other_groups:r.joinOther(i.slice(2)),profile:c,protocol:u,hostname:h[1]||"",port:h[2]||String("ssh"===u?22:23),username:"<default>"===h[3]?"":h[3]||"",auth:"",algorithms:"",settings:""};if("ssh"===u){const e={};h.length>5&&(e.x11="-1"===h[5]),Object.keys(e).length&&(m.settings=JSON.stringify(e)),(h[7]||h[8]||h[14]||h[20]||h[26])&&n.push("Advanced settings not transferred: "+c)}try{s.push(r.validateRow(m,s.length+1))}catch(e){n.push("Ignored invalid session "+c+": "+String(e.message||e))}if(s.length>r.MAX_ROWS)throw Error("Too many MobaXterm sessions")}if(!l)throw Error("No MobaXterm [Bookmarks] sections found");return{rows:s,warnings:n}},writeMoba:function(e){if(!Array.isArray(e)||!e.length||e.length>r.MAX_ROWS)throw Error("Select 1 to 10000 profiles");const t=[],o=new Map;function s(e){const r=e.join("");if(!o.has(r)){const s={path:[...e],sessions:[]};t.push(s),o.set(r,s)}return o.get(r)}s([]);for(const t of e){const e=Array.isArray(t.path)?t.path:r.validateRow(t,1).path;l(e);for(let t=1;t<=e.length;t++)s(e.slice(0,t));const o=Array.isArray(t.path)?t:r.validateRow(t,1);s(e).sessions.push(c(o))}return function(e){const t=[];for(const o of e){const e=o.codePointAt(0);if(e<=127||e>=160&&e<=255)t.push(e);else{if(!i.has(e))throw Error("MobaXterm Windows-1252 cannot encode character U+"+e.toString(16).toUpperCase()+". Rename the affected group/session.");t.push(i.get(e))}}return new Uint8Array(t)}(t.map((e,t)=>[t?"[Bookmarks_"+t+"]":"[Bookmarks]","SubRep="+e.path.join("\\"),"ImgNum="+(t?"41":"42"),...e.sessions].join("\r\n")).join("\r\n")+"\r\n")}}},804(e){"use strict";e.exports=require("@angular/common")},430(e){"use strict";e.exports=require("@angular/core")},440(e){"use strict";e.exports=require("@angular/forms")},643(e){"use strict";e.exports=require("@ng-bootstrap/ng-bootstrap")},21(e){"use strict";e.exports=require("fs")},368(e){"use strict";e.exports=require("tabby-core")},262(e){"use strict";e.exports=require("tabby-settings")},330(e){"use strict";e.exports={rE:"0.2.2"}}};const t={};let o=function o(r){const s=t[r];if(void 0!==s)return s.exports;const n=t[r]={exports:{}};return e[r].call(n.exports,n,n.exports,o),n.exports}(729);module.exports=o})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabby-profile-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Import and export SSH/Telnet profiles between Tabby, CSV and MobaXterm while preserving nested groups",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tabby-plugin",
|
|
7
|
+
"tabby",
|
|
8
|
+
"terminal",
|
|
9
|
+
"ssh",
|
|
10
|
+
"telnet",
|
|
11
|
+
"csv",
|
|
12
|
+
"profile-import",
|
|
13
|
+
"profile-export",
|
|
14
|
+
"mobaxterm",
|
|
15
|
+
"profile-manager",
|
|
16
|
+
"mxtsessions"
|
|
17
|
+
],
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "webpack --mode production",
|
|
22
|
+
"test": "node --test test/*.test.js",
|
|
23
|
+
"check": "npm test && npm run build",
|
|
24
|
+
"prepublishOnly": "npm run check"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@angular/core": "^15.0.0",
|
|
34
|
+
"@angular/common": "^15.0.0",
|
|
35
|
+
"@angular/forms": "^15.0.0",
|
|
36
|
+
"@ng-bootstrap/ng-bootstrap": "^14.0.0",
|
|
37
|
+
"@types/node": "^18.0.0",
|
|
38
|
+
"apply-loader": "^2.0.0",
|
|
39
|
+
"pug": "^3.0.0",
|
|
40
|
+
"pug-loader": "^2.4.0",
|
|
41
|
+
"ts-loader": "^9.5.0",
|
|
42
|
+
"typescript": "~4.9.5",
|
|
43
|
+
"webpack": "^5.90.0",
|
|
44
|
+
"webpack-cli": "^5.1.0",
|
|
45
|
+
"tabby-core": "^1.0.231-nightly.0",
|
|
46
|
+
"tabby-settings": "^1.0.231-nightly.0",
|
|
47
|
+
"@angular/compiler": "^15.0.0",
|
|
48
|
+
"js-yaml": "^4.1.0"
|
|
49
|
+
},
|
|
50
|
+
"author": "gugahartmann",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/gugahartmann/Tabby-Profile-Bridge.git"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/gugahartmann/Tabby-Profile-Bridge#readme",
|
|
56
|
+
"license": "MIT",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/gugahartmann/Tabby-Profile-Bridge/issues"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|