trello-cli-unofficial 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [0.17.0](https://github.com/JaegerCaiser/trello-cli-unofficial/compare/v0.16.0...v0.17.0) (2026-04-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * handle SIGINT in auth setup prompt with translated cancel message ([f352e4f](https://github.com/JaegerCaiser/trello-cli-unofficial/commit/f352e4f5c4fe62b77573cbdb915cddbd4ae275cb))
7
+
8
+
9
+ ### Features
10
+
11
+ * **search:** support advanced card search filters and pagination ([a5ebf31](https://github.com/JaegerCaiser/trello-cli-unofficial/commit/a5ebf31c434b4168d9745be1a99331dd536704a8))
12
+
1
13
  # [0.16.0](https://github.com/JaegerCaiser/trello-cli-unofficial/compare/v0.15.1...v0.16.0) (2026-04-09)
2
14
 
3
15
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Trello CLI Unofficial
2
2
 
3
+ *Read this in other languages: [English](#-english) | [Português](#-português)*
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/trello-cli-unofficial.svg)](https://www.npmjs.com/package/trello-cli-unofficial)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
7
  [![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=flat&logo=bun&logoColor=white)](https://bun.sh)
@@ -8,9 +10,93 @@
8
10
  [![CI/CD](https://img.shields.io/github/actions/workflow/status/JaegerCaiser/trello-cli-unofficial/ci.yml?branch=main&label=CI)](https://github.com/JaegerCaiser/trello-cli-unofficial/actions)
9
11
  [![Release](https://img.shields.io/github/actions/workflow/status/JaegerCaiser/trello-cli-unofficial/release.yml?branch=main&label=Release)](https://github.com/JaegerCaiser/trello-cli-unofficial/actions)
10
12
 
13
+ ---
14
+
15
+ ## 🇺🇸 English
16
+
17
+ An unofficial Trello CLI with Power-Up authentication, built with Bun and TypeScript. It allows you to manage boards, lists, and cards with modern outputs and multi-profile support.
18
+
19
+ ### Quick Start
20
+
21
+ ```bash
22
+ npm install -g trello-cli-unofficial
23
+ ```
24
+
25
+ ```bash
26
+ tcu setup
27
+ tcu interactive
28
+ ```
29
+
30
+ ### 🔑 Configuring the Trello Token
31
+
32
+ On the first run, the CLI will ask for your Trello token (via Power-Up authentication). Follow these steps:
33
+
34
+ **Step 1 — Access the Power-Ups admin dashboard:**
35
+ Open in your browser: **https://trello.com/power-ups/admin**
36
+ Log in to Trello if necessary.
37
+
38
+ **Step 2 — Create a new Power-Up:**
39
+ Click on **"New"** and fill in any name (e.g., `My CLI`). The name doesn't matter — it's just to identify the app in your account.
40
+ > If you have created a Power-Up before, simply select it from the list.
41
+
42
+ **Step 3 — Generate the token manually:**
43
+ Inside your Power-Up's page, you will see a message on the right panel:
44
+ > *"Most developers will need to ask each user to authorize their app. If you are building an app for yourself or doing local testing, you can manually generate a **token**."*
45
+
46
+ Click on the highlighted word **token** in that message. A new page will open asking for confirmation — click **"Allow"**.
47
+
48
+ **Step 4 — Copy the token:**
49
+ The page will display a long sequence of characters starting with `ATTA`. Copy the entire string.
50
+
51
+ **Step 5 — Paste it into the terminal:**
52
+ When the CLI asks `Please enter your Trello token:`, paste the token and press Enter.
53
+
54
+ > **Tip:** The token must always start with `ATTA`. If the CLI rejects your input, make sure you copied the full token without any extra spaces.
55
+
56
+ > **Security:** The token is saved locally in `~/.trello-cli-unofficial/config.json`. To revoke access, go to [trello.com/account](https://trello.com/account) → **Power-Ups & Tokens** and revoke the token for this application.
57
+
58
+ ### Features
59
+
60
+ - 🚀 Interactive mode with a guided menu
61
+ - 📊 Output in JSON, table, and CSV formats
62
+ - 🌍 i18n support (English / pt-BR)
63
+ - 🔐 Power-Up authentication for Trello
64
+ - 👤 Multi-profile support with persistent configuration
65
+
66
+ ### Commands
67
+
68
+ ```bash
69
+ tcu boards
70
+
71
+ tcu lists
72
+
73
+ tcu cards
74
+
75
+ tcu checklists
76
+
77
+ tcu config
78
+ ```
79
+
80
+ *To see all available parameters and options, run `tcu <command> --help` in your terminal.*
81
+
82
+ ### 📚 Full Documentation
83
+
84
+ - [./docs/commands.md](./docs/commands.md)
85
+ - [./docs/architecture.md](./docs/architecture.md)
86
+ - [./docs/migration-guide.md](./docs/migration-guide.md)
87
+ - [./docs/troubleshooting.md](./docs/troubleshooting.md)
88
+
89
+ ### Contributing
90
+
91
+ Want to help improve the project? See our [Contributing Guide](CONTRIBUTING.md).
92
+
93
+ ---
94
+
95
+ ## 🇧🇷 Português
96
+
11
97
  Um CLI não-oficial para Trello com autenticação Power-Up, feito em Bun e TypeScript. Permite gerenciar boards, lists e cards com saídas modernas e suporte a múltiplos perfis.
12
98
 
13
- ## Quick Start
99
+ ### Quick Start
14
100
 
15
101
  ```bash
16
102
  npm install -g trello-cli-unofficial
@@ -21,43 +107,35 @@ tcu setup
21
107
  tcu interactive
22
108
  ```
23
109
 
24
- ## 🔑 Configurando o Token do Trello
110
+ ### 🔑 Configurando o Token do Trello
25
111
 
26
112
  Na primeira execução o CLI vai pedir seu token do Trello (autenticação via Power-Up). Siga os passos abaixo:
27
113
 
28
114
  **Passo 1 — Acesse o painel de administração de Power-Ups:**
29
-
30
115
  Abra no navegador: **https://trello.com/power-ups/admin**
31
-
32
116
  Faça login no Trello se necessário.
33
117
 
34
118
  **Passo 2 — Crie um novo Power-Up:**
35
-
36
119
  Clique em **"New"** e preencha com qualquer nome (ex: `Meu CLI`). O nome não importa — é só para identificar o app na sua conta.
37
-
38
120
  > Se você já criou um Power-Up antes, basta selecioná-lo na lista.
39
121
 
40
122
  **Passo 3 — Gere o token manualmente:**
41
-
42
123
  Dentro da página do seu Power-Up, você verá no painel à direita a mensagem:
43
-
44
124
  > *"A maioria dos desenvolvedores precisará solicitar a cada usuário que autorize seu aplicativo. Se você deseja criar um aplicativo para si mesmo ou está fazendo testes locais, é possível gerar um **token** manualmente."*
45
125
 
46
126
  Clique na palavra **token** destacada nessa mensagem. Uma nova página abrirá pedindo confirmação — clique em **"Permitir"**.
47
127
 
48
128
  **Passo 4 — Copie o token:**
49
-
50
129
  A página exibirá uma longa sequência de caracteres começando com `ATTA`. Copie tudo.
51
130
 
52
131
  **Passo 5 — Cole no terminal:**
53
-
54
132
  Quando o CLI perguntar `Por favor, insira seu token do Trello:`, cole o token e pressione Enter.
55
133
 
56
134
  > **Dica:** O token começa obrigatoriamente com `ATTA`. Se o CLI rejeitar sua entrada, verifique se copiou o token completo sem espaços extras.
57
135
 
58
136
  > **Segurança:** O token fica salvo localmente em `~/.trello-cli-unofficial/config.json`. Para revogar o acesso, acesse [trello.com/account](https://trello.com/account) → **Power-Ups & Tokens** e revogue o token desta aplicação.
59
137
 
60
- ## Features
138
+ ### Features
61
139
 
62
140
  - 🚀 Modo interativo com menu guiado
63
141
  - 📊 Saída em JSON, table e CSV
@@ -65,7 +143,7 @@ Quando o CLI perguntar `Por favor, insira seu token do Trello:`, cole o token e
65
143
  - 🔐 Autenticação Power-Up para Trello
66
144
  - 👤 Suporte a múltiplos perfis e configuração persistente
67
145
 
68
- ## Commands
146
+ ### Commands
69
147
 
70
148
  ```bash
71
149
  tcu boards
@@ -81,13 +159,13 @@ tcu config
81
159
 
82
160
  *Para ver todos os parâmetros e opções, execute `tcu <comando> --help` no seu terminal.*
83
161
 
84
- ## 📚 Documentação Completa
162
+ ### 📚 Documentação Completa
85
163
 
86
164
  - [./docs/commands.md](./docs/commands.md)
87
165
  - [./docs/architecture.md](./docs/architecture.md)
88
166
  - [./docs/migration-guide.md](./docs/migration-guide.md)
89
167
  - [./docs/troubleshooting.md](./docs/troubleshooting.md)
90
168
 
91
- ## Contribuição
169
+ ### Contribuição
92
170
 
93
- Quer ajudar a melhorar o projeto? Veja nosso [Guia de Contribuição](CONTRIBUTING.md).
171
+ Quer ajudar a melhorar o projeto? Veja nosso [Guia de Contribuição](CONTRIBUTING.md).
package/dist/main.js CHANGED
@@ -2850,6 +2850,7 @@ var require_en = __commonJS((exports, module) => {
2850
2850
  enterToken: "Please enter your Trello token:",
2851
2851
  tokenInvalid: "❌ Invalid token! Token must start with 'ATTA' and be at least 11 characters long.",
2852
2852
  tokenSaved: "✅ Token saved successfully!",
2853
+ setupCancelled: "⚠️ Setup cancelled by the user.",
2853
2854
  authenticated: "✅ You are already authenticated!"
2854
2855
  },
2855
2856
  menu: {
@@ -2955,7 +2956,8 @@ var require_en = __commonJS((exports, module) => {
2955
2956
  validation: {
2956
2957
  requiredName: "Name is required",
2957
2958
  requiredCardName: "Card name is required",
2958
- cardNameCannotBeEmpty: "Card name cannot be empty"
2959
+ cardNameCannotBeEmpty: "Card name cannot be empty",
2960
+ requiredSearchQuery: "Search query cannot be empty"
2959
2961
  },
2960
2962
  actions: {
2961
2963
  view: "👁️ View Details",
@@ -2977,6 +2979,11 @@ var require_en = __commonJS((exports, module) => {
2977
2979
  checklistProgress: " [{{done}}/{{total}}]",
2978
2980
  checklistItemDone: " ☑ {{name}}",
2979
2981
  checklistItemPending: " ☐ {{name}}"
2982
+ },
2983
+ search: {
2984
+ results: '🔍 Results for "{{query}}" ({{count}} found):',
2985
+ empty: '🔍 No cards found for "{{query}}".',
2986
+ pagination: "📄 Page {{page}} (limit: {{limit}})"
2980
2987
  }
2981
2988
  },
2982
2989
  checklist: {
@@ -3144,6 +3151,14 @@ var require_en = __commonJS((exports, module) => {
3144
3151
  },
3145
3152
  delete: {
3146
3153
  description: "Delete a card"
3154
+ },
3155
+ search: {
3156
+ description: "Search cards by text",
3157
+ boardIdOption: "Filter by board ID",
3158
+ listIdOption: "Filter by list ID",
3159
+ labelsOption: "Filter by labels (comma-separated)",
3160
+ limitOption: "Maximum number of results",
3161
+ pageOption: "Page number (starts at 0)"
3147
3162
  }
3148
3163
  },
3149
3164
  checklists: {
@@ -3218,6 +3233,7 @@ var require_pt_BR = __commonJS((exports, module) => {
3218
3233
  enterToken: "Por favor, insira seu token do Trello:",
3219
3234
  tokenInvalid: "❌ Token inválido! O token deve começar com 'ATTA' e ter pelo menos 11 caracteres.",
3220
3235
  tokenSaved: "✅ Token salvo com sucesso!",
3236
+ setupCancelled: "⚠️ Configuração cancelada pelo usuário.",
3221
3237
  authenticated: "✅ Você já está autenticado!"
3222
3238
  },
3223
3239
  menu: {
@@ -3323,7 +3339,8 @@ var require_pt_BR = __commonJS((exports, module) => {
3323
3339
  validation: {
3324
3340
  requiredName: "Nome é obrigatório",
3325
3341
  requiredCardName: "Nome do cartão é obrigatório",
3326
- cardNameCannotBeEmpty: "Nome do cartão não pode estar vazio"
3342
+ cardNameCannotBeEmpty: "Nome do cartão não pode estar vazio",
3343
+ requiredSearchQuery: "A consulta de busca não pode estar vazia"
3327
3344
  },
3328
3345
  actions: {
3329
3346
  view: "👁️ Ver Detalhes",
@@ -3345,6 +3362,11 @@ var require_pt_BR = __commonJS((exports, module) => {
3345
3362
  checklistProgress: " [{{done}}/{{total}}]",
3346
3363
  checklistItemDone: " ☑ {{name}}",
3347
3364
  checklistItemPending: " ☐ {{name}}"
3365
+ },
3366
+ search: {
3367
+ results: '🔍 Resultados para "{{query}}" ({{count}} encontrado(s)):',
3368
+ empty: '🔍 Nenhum card encontrado para "{{query}}".',
3369
+ pagination: "📄 Página {{page}} (limite: {{limit}})"
3348
3370
  }
3349
3371
  },
3350
3372
  checklist: {
@@ -3512,6 +3534,14 @@ var require_pt_BR = __commonJS((exports, module) => {
3512
3534
  },
3513
3535
  delete: {
3514
3536
  description: "Deleta um card"
3537
+ },
3538
+ search: {
3539
+ description: "Buscar cards por texto",
3540
+ boardIdOption: "Filtrar por ID de board",
3541
+ listIdOption: "Filtrar por ID de lista",
3542
+ labelsOption: "Filtrar por labels (separadas por vírgula)",
3543
+ limitOption: "Número máximo de resultados",
3544
+ pageOption: "Número da página (começa em 0)"
3515
3545
  }
3516
3546
  },
3517
3547
  checklists: {
@@ -3954,6 +3984,37 @@ var init_RenameChecklistUseCase = __esm(() => {
3954
3984
  init_i18n();
3955
3985
  });
3956
3986
 
3987
+ // src/application/use-cases/SearchCardsUseCase.ts
3988
+ class SearchCardsUseCase {
3989
+ trelloRepository;
3990
+ constructor(trelloRepository) {
3991
+ this.trelloRepository = trelloRepository;
3992
+ }
3993
+ async execute(query, filters) {
3994
+ if (!query || query.trim().length === 0) {
3995
+ throw new Error(t3("card.validation.requiredSearchQuery"));
3996
+ }
3997
+ let fullQuery = query.trim();
3998
+ if (filters?.labels) {
3999
+ const labelTerms = filters.labels.split(",").map((l) => `label:"${l.trim()}"`).join(" ");
4000
+ fullQuery = `${fullQuery} ${labelTerms}`;
4001
+ }
4002
+ const options = {
4003
+ boardId: filters?.boardId,
4004
+ limit: filters?.limit,
4005
+ page: filters?.page
4006
+ };
4007
+ const results = await this.trelloRepository.searchCards(fullQuery, options);
4008
+ if (filters?.listId) {
4009
+ return results.filter((card) => card.idList === filters.listId);
4010
+ }
4011
+ return results;
4012
+ }
4013
+ }
4014
+ var init_SearchCardsUseCase = __esm(() => {
4015
+ init_i18n();
4016
+ });
4017
+
3957
4018
  // src/application/use-cases/UpdateCardUseCase.ts
3958
4019
  class UpdateCardUseCase {
3959
4020
  trelloRepository;
@@ -3993,6 +4054,7 @@ var init_use_cases = __esm(() => {
3993
4054
  init_GetBoardDetailsUseCase();
3994
4055
  init_RenameChecklistItemUseCase();
3995
4056
  init_RenameChecklistUseCase();
4057
+ init_SearchCardsUseCase();
3996
4058
  init_UpdateCardUseCase();
3997
4059
  });
3998
4060
 
@@ -25646,16 +25708,24 @@ class AuthController {
25646
25708
  console.log(t3("auth.tokenHintUrl"));
25647
25709
  console.log(t3("auth.tokenHintStep2"));
25648
25710
  console.log(t3("auth.tokenHintStep3"));
25649
- const { token } = await dist_default14.prompt([
25650
- {
25651
- type: "input",
25652
- name: "token",
25653
- message: t3("auth.enterToken"),
25654
- validate: (input) => input.startsWith("ATTA") || t3("auth.tokenInvalid")
25711
+ try {
25712
+ const { token } = await dist_default14.prompt([
25713
+ {
25714
+ type: "input",
25715
+ name: "token",
25716
+ message: t3("auth.enterToken"),
25717
+ validate: (input) => input.startsWith("ATTA") || t3("auth.tokenInvalid")
25718
+ }
25719
+ ]);
25720
+ const result = await this.authenticateUseCase.execute(token);
25721
+ console.log(result.success ? t3("auth.tokenSaved") : result.message);
25722
+ } catch (error) {
25723
+ if (error instanceof Error && error.name === "ExitPromptError") {
25724
+ console.log(t3("auth.setupCancelled"));
25725
+ process.exit(0);
25655
25726
  }
25656
- ]);
25657
- const result = await this.authenticateUseCase.execute(token);
25658
- console.log(result.success ? t3("auth.tokenSaved") : result.message);
25727
+ throw error;
25728
+ }
25659
25729
  }
25660
25730
  async getConfig() {
25661
25731
  return await this.authenticateUseCase.getConfig();
@@ -25875,6 +25945,7 @@ class CardController {
25875
25945
  deleteCardUseCase;
25876
25946
  moveCardUseCase;
25877
25947
  getCardUseCase;
25948
+ searchCardsUseCase;
25878
25949
  constructor(trelloRepository, boardController, outputFormatter) {
25879
25950
  this.trelloRepository = trelloRepository;
25880
25951
  this.boardController = boardController;
@@ -25884,6 +25955,7 @@ class CardController {
25884
25955
  this.deleteCardUseCase = new DeleteCardUseCase(trelloRepository);
25885
25956
  this.moveCardUseCase = new MoveCardUseCase(trelloRepository);
25886
25957
  this.getCardUseCase = new GetCardUseCase(trelloRepository);
25958
+ this.searchCardsUseCase = new SearchCardsUseCase(trelloRepository);
25887
25959
  }
25888
25960
  async createCardInteractive() {
25889
25961
  const boards = await this.boardController.getBoards();
@@ -26096,28 +26168,7 @@ class CardController {
26096
26168
  throw new Error(t3("card.notFound", { cardId }));
26097
26169
  }
26098
26170
  async deleteCard(cardId) {
26099
- const boards = await this.trelloRepository.getBoards();
26100
- let card;
26101
- for (const board of boards) {
26102
- const lists = await this.trelloRepository.getLists(board.id);
26103
- for (const list of lists) {
26104
- try {
26105
- const cards = await this.trelloRepository.getCards(list.id);
26106
- card = cards.find((c) => c.id === cardId);
26107
- if (card) {
26108
- break;
26109
- }
26110
- } catch {
26111
- continue;
26112
- }
26113
- }
26114
- if (card) {
26115
- break;
26116
- }
26117
- }
26118
- if (!card) {
26119
- throw new Error(t3("card.notFound", { cardId }));
26120
- }
26171
+ const card = await this.trelloRepository.getCard(cardId);
26121
26172
  await this.deleteCardUseCase.execute(cardId);
26122
26173
  console.log(t3("card.deleted"));
26123
26174
  console.log(t3("card.cardName", { name: card.name }));
@@ -26230,44 +26281,32 @@ class CardController {
26230
26281
  }
26231
26282
  }
26232
26283
  async moveCardToList(cardId, targetListId) {
26233
- const boards = await this.trelloRepository.getBoards();
26234
- let card;
26235
- for (const board of boards) {
26236
- const lists = await this.trelloRepository.getLists(board.id);
26237
- for (const list of lists) {
26238
- try {
26239
- const cards = await this.trelloRepository.getCards(list.id);
26240
- card = cards.find((c) => c.id === cardId);
26241
- if (card) {
26242
- break;
26243
- }
26244
- } catch {
26245
- continue;
26246
- }
26247
- }
26248
- if (card) {
26249
- break;
26250
- }
26251
- }
26252
- if (!card) {
26253
- throw new Error(t3("card.notFound", { cardId }));
26254
- }
26255
- let targetList;
26256
- for (const board of boards) {
26257
- const lists = await this.trelloRepository.getLists(board.id);
26258
- targetList = lists.find((l) => l.id === targetListId);
26259
- if (targetList) {
26260
- break;
26261
- }
26262
- }
26263
- if (!targetList) {
26264
- throw new Error(t3("list.notFoundById", { listId: targetListId }));
26265
- }
26284
+ const [card, targetList] = await Promise.all([
26285
+ this.trelloRepository.getCard(cardId),
26286
+ this.trelloRepository.getList(targetListId)
26287
+ ]);
26266
26288
  await this.moveCardUseCase.execute(cardId, targetListId);
26267
26289
  console.log(t3("card.moved"));
26268
26290
  console.log(t3("card.cardName", { name: card.name }));
26269
26291
  console.log(t3("card.movedTo", { listName: targetList.name }));
26270
26292
  }
26293
+ async searchCards(query, filters) {
26294
+ const cards = await this.searchCardsUseCase.execute(query, filters);
26295
+ if (cards.length === 0) {
26296
+ console.log(t3("card.search.empty", { query }));
26297
+ return;
26298
+ }
26299
+ console.log(t3("card.search.results", { query, count: cards.length }));
26300
+ if (filters?.page !== undefined || filters?.limit !== undefined) {
26301
+ const page = (filters.page ?? 0) + 1;
26302
+ const limit = filters.limit ?? 50;
26303
+ console.log(t3("card.search.pagination", { page, limit }));
26304
+ }
26305
+ this.outputFormatter.output(cards, {
26306
+ fields: ["name", "id", "url"],
26307
+ headers: ["Name", "ID", "URL"]
26308
+ });
26309
+ }
26271
26310
  }
26272
26311
  var init_CardController = __esm(() => {
26273
26312
  init_use_cases();
@@ -28725,6 +28764,10 @@ ${errorText}`);
28725
28764
  const data = await this.requestLists(`/boards/${boardId}/lists`);
28726
28765
  return data.map((list) => ListEntity.fromApiResponse(list));
28727
28766
  }
28767
+ async getList(listId) {
28768
+ const data = await this.request(`/lists/${listId}`);
28769
+ return ListEntity.fromApiResponse(data);
28770
+ }
28728
28771
  async createList(boardId, name) {
28729
28772
  const body = new URLSearchParams({
28730
28773
  idBoard: boardId,
@@ -28907,6 +28950,16 @@ ${errorText}`);
28907
28950
  });
28908
28951
  return ChecklistItemEntity.fromApiResponse(data);
28909
28952
  }
28953
+ async searchCards(query, options) {
28954
+ const limit = options?.limit ?? 50;
28955
+ const page = options?.page ?? 0;
28956
+ let endpoint = `/search?query=${encodeURIComponent(query)}&modelTypes=cards&cards_limit=${limit}&cards_page=${page}&card_fields=id,name,desc,idList,url,pos`;
28957
+ if (options?.boardId) {
28958
+ endpoint += `&idBoards=${encodeURIComponent(options.boardId)}`;
28959
+ }
28960
+ const data = await this.request(endpoint);
28961
+ return data.cards.map((card) => CardEntity.fromApiResponse(card));
28962
+ }
28910
28963
  }
28911
28964
  var init_TrelloApiRepository = __esm(() => {
28912
28965
  init_entities();
@@ -31546,6 +31599,21 @@ class CommandController {
31546
31599
  await this.cardController.showCard(cardId);
31547
31600
  }, "cards show");
31548
31601
  });
31602
+ cardsCmd.command("search <query>").description(t3("commands.cards.search.description")).option("-f, --format <format>", t3("commands.formatOption"), "table").option("--board-id <boardId>", t3("commands.cards.search.boardIdOption")).option("--list-id <listId>", t3("commands.cards.search.listIdOption")).option("--labels <labels>", t3("commands.cards.search.labelsOption")).option("--limit <limit>", t3("commands.cards.search.limitOption")).option("--page <page>", t3("commands.cards.search.pageOption")).action(async (query, options) => {
31603
+ await ErrorHandler.withErrorHandling(async () => {
31604
+ await this.initializeTrelloControllers();
31605
+ if (options.format) {
31606
+ this.outputFormatter.setFormat(options.format);
31607
+ }
31608
+ await this.cardController.searchCards(query, {
31609
+ boardId: options.boardId,
31610
+ listId: options.listId,
31611
+ labels: options.labels,
31612
+ limit: options.limit !== undefined ? Number.parseInt(options.limit, 10) : undefined,
31613
+ page: options.page !== undefined ? Number.parseInt(options.page, 10) : undefined
31614
+ });
31615
+ }, "cards search");
31616
+ });
31549
31617
  cardsCmd.command("legacy <boardName> <listName>").description(t3("commands.deprecated.cardsAliasDescription")).action(async (boardName, listName) => {
31550
31618
  console.warn(t3("commands.deprecated.cardsAliasWarning"));
31551
31619
  await ErrorHandler.withErrorHandling(async () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trello-cli-unofficial",
3
3
  "type": "module",
4
- "version": "0.16.0",
4
+ "version": "0.17.0",
5
5
  "private": false,
6
6
  "packageManager": "bun@1.3.11",
7
7
  "description": "Unofficial Trello CLI using Power-Up authentication, built with Bun for maximum performance",
@@ -0,0 +1,41 @@
1
+ import type { CardEntity } from '@domain/entities';
2
+ import type { SearchCardsOptions, TrelloRepository } from '@domain/repositories';
3
+ import { t } from '@/i18n';
4
+
5
+ export interface SearchCardsFilters {
6
+ boardId?: string;
7
+ listId?: string;
8
+ labels?: string;
9
+ limit?: number;
10
+ page?: number;
11
+ }
12
+
13
+ export class SearchCardsUseCase {
14
+ constructor(private trelloRepository: TrelloRepository) {}
15
+
16
+ async execute(query: string, filters?: SearchCardsFilters): Promise<CardEntity[]> {
17
+ if (!query || query.trim().length === 0) {
18
+ throw new Error(t('card.validation.requiredSearchQuery'));
19
+ }
20
+
21
+ let fullQuery = query.trim();
22
+ if (filters?.labels) {
23
+ const labelTerms = filters.labels.split(',').map(l => `label:"${l.trim()}"`).join(' ');
24
+ fullQuery = `${fullQuery} ${labelTerms}`;
25
+ }
26
+
27
+ const options: SearchCardsOptions = {
28
+ boardId: filters?.boardId,
29
+ limit: filters?.limit,
30
+ page: filters?.page,
31
+ };
32
+
33
+ const results = await this.trelloRepository.searchCards(fullQuery, options);
34
+
35
+ if (filters?.listId) {
36
+ return results.filter(card => card.idList === filters.listId);
37
+ }
38
+
39
+ return results;
40
+ }
41
+ }
@@ -15,5 +15,6 @@ export * from './GetListsUseCase';
15
15
  export * from './MoveCardUseCase';
16
16
  export * from './RenameChecklistItemUseCase';
17
17
  export * from './RenameChecklistUseCase';
18
+ export * from './SearchCardsUseCase';
18
19
  export * from './UpdateCardUseCase';
19
20
  export * from './UpdateChecklistItemStateUseCase';
@@ -21,12 +21,19 @@ export interface BoardLabel {
21
21
  color: string;
22
22
  }
23
23
 
24
+ export interface SearchCardsOptions {
25
+ boardId?: string;
26
+ limit?: number;
27
+ page?: number;
28
+ }
29
+
24
30
  export interface TrelloRepository {
25
31
  getBoards: () => Promise<BoardEntity[]>;
26
32
  createBoard: (name: string, description?: string) => Promise<BoardEntity>;
27
33
  getBoardMembers: (boardId: string) => Promise<BoardMember[]>;
28
34
  getBoardLabels: (boardId: string) => Promise<BoardLabel[]>;
29
35
  getLists: (boardId: string) => Promise<ListEntity[]>;
36
+ getList: (listId: string) => Promise<ListEntity>;
30
37
  createList: (boardId: string, name: string) => Promise<ListEntity>;
31
38
  deleteList: (listId: string) => Promise<void>;
32
39
  moveList: (listId: string, position: number) => Promise<ListEntity>;
@@ -43,4 +50,5 @@ export interface TrelloRepository {
43
50
  deleteChecklistItem: (checklistId: string, itemId: string) => Promise<void>;
44
51
  renameChecklistItem: (cardId: string, itemId: string, name: string) => Promise<ChecklistItemEntity>;
45
52
  updateChecklistItemState: (cardId: string, itemId: string, state: 'complete' | 'incomplete') => Promise<ChecklistItemEntity>;
53
+ searchCards: (query: string, options?: SearchCardsOptions) => Promise<CardEntity[]>;
46
54
  }
@@ -20,6 +20,7 @@
20
20
  "enterToken": "Please enter your Trello token:",
21
21
  "tokenInvalid": "❌ Invalid token! Token must start with 'ATTA' and be at least 11 characters long.",
22
22
  "tokenSaved": "✅ Token saved successfully!",
23
+ "setupCancelled": "⚠️ Setup cancelled by the user.",
23
24
  "authenticated": "✅ You are already authenticated!"
24
25
  },
25
26
  "menu": {
@@ -125,7 +126,8 @@
125
126
  "validation": {
126
127
  "requiredName": "Name is required",
127
128
  "requiredCardName": "Card name is required",
128
- "cardNameCannotBeEmpty": "Card name cannot be empty"
129
+ "cardNameCannotBeEmpty": "Card name cannot be empty",
130
+ "requiredSearchQuery": "Search query cannot be empty"
129
131
  },
130
132
  "actions": {
131
133
  "view": "👁️ View Details",
@@ -147,6 +149,11 @@
147
149
  "checklistProgress": " [{{done}}/{{total}}]",
148
150
  "checklistItemDone": " ☑ {{name}}",
149
151
  "checklistItemPending": " ☐ {{name}}"
152
+ },
153
+ "search": {
154
+ "results": "🔍 Results for \"{{query}}\" ({{count}} found):",
155
+ "empty": "🔍 No cards found for \"{{query}}\".",
156
+ "pagination": "📄 Page {{page}} (limit: {{limit}})"
150
157
  }
151
158
  },
152
159
  "checklist": {
@@ -314,6 +321,14 @@
314
321
  },
315
322
  "delete": {
316
323
  "description": "Delete a card"
324
+ },
325
+ "search": {
326
+ "description": "Search cards by text",
327
+ "boardIdOption": "Filter by board ID",
328
+ "listIdOption": "Filter by list ID",
329
+ "labelsOption": "Filter by labels (comma-separated)",
330
+ "limitOption": "Maximum number of results",
331
+ "pageOption": "Page number (starts at 0)"
317
332
  }
318
333
  },
319
334
  "checklists": {
@@ -20,6 +20,7 @@
20
20
  "enterToken": "Por favor, insira seu token do Trello:",
21
21
  "tokenInvalid": "❌ Token inválido! O token deve começar com 'ATTA' e ter pelo menos 11 caracteres.",
22
22
  "tokenSaved": "✅ Token salvo com sucesso!",
23
+ "setupCancelled": "⚠️ Configuração cancelada pelo usuário.",
23
24
  "authenticated": "✅ Você já está autenticado!"
24
25
  },
25
26
  "menu": {
@@ -125,7 +126,8 @@
125
126
  "validation": {
126
127
  "requiredName": "Nome é obrigatório",
127
128
  "requiredCardName": "Nome do cartão é obrigatório",
128
- "cardNameCannotBeEmpty": "Nome do cartão não pode estar vazio"
129
+ "cardNameCannotBeEmpty": "Nome do cartão não pode estar vazio",
130
+ "requiredSearchQuery": "A consulta de busca não pode estar vazia"
129
131
  },
130
132
  "actions": {
131
133
  "view": "👁️ Ver Detalhes",
@@ -147,6 +149,11 @@
147
149
  "checklistProgress": " [{{done}}/{{total}}]",
148
150
  "checklistItemDone": " ☑ {{name}}",
149
151
  "checklistItemPending": " ☐ {{name}}"
152
+ },
153
+ "search": {
154
+ "results": "🔍 Resultados para \"{{query}}\" ({{count}} encontrado(s)):",
155
+ "empty": "🔍 Nenhum card encontrado para \"{{query}}\".",
156
+ "pagination": "📄 Página {{page}} (limite: {{limit}})"
150
157
  }
151
158
  },
152
159
  "checklist": {
@@ -314,6 +321,14 @@
314
321
  },
315
322
  "delete": {
316
323
  "description": "Deleta um card"
324
+ },
325
+ "search": {
326
+ "description": "Buscar cards por texto",
327
+ "boardIdOption": "Filtrar por ID de board",
328
+ "listIdOption": "Filtrar por ID de lista",
329
+ "labelsOption": "Filtrar por labels (separadas por vírgula)",
330
+ "limitOption": "Número máximo de resultados",
331
+ "pageOption": "Número da página (começa em 0)"
317
332
  }
318
333
  },
319
334
  "checklists": {
@@ -1,5 +1,5 @@
1
1
  import type { CreateCardData, UpdateCardData } from '@domain/entities';
2
- import type { TrelloRepository } from '@domain/repositories';
2
+ import type { SearchCardsOptions, TrelloRepository } from '@domain/repositories';
3
3
  import { BoardEntity, CardEntity, ChecklistEntity, ChecklistItemEntity, ListEntity } from '@domain/entities';
4
4
  import { t } from '@/i18n';
5
5
 
@@ -171,6 +171,11 @@ export class TrelloApiRepository implements TrelloRepository {
171
171
  );
172
172
  }
173
173
 
174
+ async getList(listId: string): Promise<ListEntity> {
175
+ const data = await this.request(`/lists/${listId}`);
176
+ return ListEntity.fromApiResponse(data as TrelloListResponse);
177
+ }
178
+
174
179
  async createList(boardId: string, name: string): Promise<ListEntity> {
175
180
  const body = new URLSearchParams({
176
181
  idBoard: boardId,
@@ -400,4 +405,15 @@ export class TrelloApiRepository implements TrelloRepository {
400
405
 
401
406
  return ChecklistItemEntity.fromApiResponse(data as TrelloChecklistItemResponse);
402
407
  }
408
+
409
+ async searchCards(query: string, options?: SearchCardsOptions): Promise<CardEntity[]> {
410
+ const limit = options?.limit ?? 50;
411
+ const page = options?.page ?? 0;
412
+ let endpoint = `/search?query=${encodeURIComponent(query)}&modelTypes=cards&cards_limit=${limit}&cards_page=${page}&card_fields=id,name,desc,idList,url,pos`;
413
+ if (options?.boardId) {
414
+ endpoint += `&idBoards=${encodeURIComponent(options.boardId)}`;
415
+ }
416
+ const data = await this.request(endpoint) as { cards: TrelloCardResponse[] };
417
+ return data.cards.map(card => CardEntity.fromApiResponse(card));
418
+ }
403
419
  }
@@ -26,17 +26,26 @@ export class AuthController {
26
26
  console.log(t('auth.tokenHintStep2'));
27
27
  console.log(t('auth.tokenHintStep3'));
28
28
 
29
- const { token } = await inquirer.prompt([
30
- {
31
- type: 'input',
32
- name: 'token',
33
- message: t('auth.enterToken'),
34
- validate: input => input.startsWith('ATTA') || t('auth.tokenInvalid'),
35
- },
36
- ]);
37
-
38
- const result = await this.authenticateUseCase.execute(token);
39
- console.log(result.success ? t('auth.tokenSaved') : result.message);
29
+ try {
30
+ const { token } = await inquirer.prompt([
31
+ {
32
+ type: 'input',
33
+ name: 'token',
34
+ message: t('auth.enterToken'),
35
+ validate: input => input.startsWith('ATTA') || t('auth.tokenInvalid'),
36
+ },
37
+ ]);
38
+
39
+ const result = await this.authenticateUseCase.execute(token);
40
+ console.log(result.success ? t('auth.tokenSaved') : result.message);
41
+ } catch (error: unknown) {
42
+ if (error instanceof Error && error.name === 'ExitPromptError') {
43
+ console.log(t('auth.setupCancelled'));
44
+ process.exit(0);
45
+ }
46
+
47
+ throw error;
48
+ }
40
49
  }
41
50
 
42
51
  async getConfig(): Promise<ConfigEntity> {
@@ -1,3 +1,4 @@
1
+ import type { SearchCardsFilters } from '@application/use-cases';
1
2
  import type { BoardEntity, CardEntity, ListEntity } from '@domain/entities';
2
3
  import type { TrelloRepository } from '@domain/repositories';
3
4
  import type { BoardController } from './BoardController';
@@ -8,6 +9,7 @@ import {
8
9
  DeleteCardUseCase,
9
10
  GetCardUseCase,
10
11
  MoveCardUseCase,
12
+ SearchCardsUseCase,
11
13
  UpdateCardUseCase,
12
14
  } from '@application/use-cases';
13
15
 
@@ -21,6 +23,7 @@ export class CardController {
21
23
  private deleteCardUseCase: DeleteCardUseCase;
22
24
  private moveCardUseCase: MoveCardUseCase;
23
25
  private getCardUseCase: GetCardUseCase;
26
+ private searchCardsUseCase: SearchCardsUseCase;
24
27
 
25
28
  constructor(
26
29
  private trelloRepository: TrelloRepository,
@@ -32,6 +35,7 @@ export class CardController {
32
35
  this.deleteCardUseCase = new DeleteCardUseCase(trelloRepository);
33
36
  this.moveCardUseCase = new MoveCardUseCase(trelloRepository);
34
37
  this.getCardUseCase = new GetCardUseCase(trelloRepository);
38
+ this.searchCardsUseCase = new SearchCardsUseCase(trelloRepository);
35
39
  }
36
40
 
37
41
  async createCardInteractive(): Promise<void> {
@@ -309,34 +313,7 @@ export class CardController {
309
313
  }
310
314
 
311
315
  async deleteCard(cardId: string): Promise<void> {
312
- // Primeiro precisamos encontrar o cartão para mostrar informações
313
- const boards = await this.trelloRepository.getBoards();
314
- let card: CardEntity | undefined;
315
-
316
- for (const board of boards) {
317
- const lists = await this.trelloRepository.getLists(board.id);
318
-
319
- for (const list of lists) {
320
- try {
321
- const cards = await this.trelloRepository.getCards(list.id);
322
- card = cards.find((c: CardEntity) => c.id === cardId);
323
-
324
- if (card) {
325
- break;
326
- }
327
- } catch {
328
- continue;
329
- }
330
- }
331
- if (card) {
332
- break;
333
- }
334
- }
335
-
336
- if (!card) {
337
- throw new Error(t('card.notFound', { cardId }));
338
- }
339
-
316
+ const card = await this.trelloRepository.getCard(cardId);
340
317
  await this.deleteCardUseCase.execute(cardId);
341
318
  console.log(t('card.deleted'));
342
319
  console.log(t('card.cardName', { name: card.name }));
@@ -494,51 +471,32 @@ export class CardController {
494
471
  }
495
472
 
496
473
  async moveCardToList(cardId: string, targetListId: string): Promise<void> {
497
- // Primeiro precisamos encontrar o cartão para mostrar informações
498
- const boards = await this.trelloRepository.getBoards();
499
- let card: CardEntity | undefined;
500
-
501
- for (const board of boards) {
502
- const lists = await this.trelloRepository.getLists(board.id);
503
-
504
- for (const list of lists) {
505
- try {
506
- const cards = await this.trelloRepository.getCards(list.id);
507
- card = cards.find((c: CardEntity) => c.id === cardId);
508
-
509
- if (card) {
510
- break;
511
- }
512
- } catch {
513
- continue;
514
- }
515
- }
516
- if (card) {
517
- break;
518
- }
519
- }
520
-
521
- if (!card) {
522
- throw new Error(t('card.notFound', { cardId }));
523
- }
524
-
525
- // Verificar se a lista de destino existe procurando em todos os boards
526
- let targetList: ListEntity | undefined;
527
- for (const board of boards) {
528
- const lists = await this.trelloRepository.getLists(board.id);
529
- targetList = lists.find((l: ListEntity) => l.id === targetListId);
530
- if (targetList) {
531
- break;
532
- }
533
- }
534
-
535
- if (!targetList) {
536
- throw new Error(t('list.notFoundById', { listId: targetListId }));
537
- }
474
+ const [card, targetList] = await Promise.all([
475
+ this.trelloRepository.getCard(cardId),
476
+ this.trelloRepository.getList(targetListId),
477
+ ]);
538
478
 
539
479
  await this.moveCardUseCase.execute(cardId, targetListId);
540
480
  console.log(t('card.moved'));
541
481
  console.log(t('card.cardName', { name: card.name }));
542
482
  console.log(t('card.movedTo', { listName: targetList.name }));
543
483
  }
484
+
485
+ async searchCards(query: string, filters?: SearchCardsFilters): Promise<void> {
486
+ const cards = await this.searchCardsUseCase.execute(query, filters);
487
+ if (cards.length === 0) {
488
+ console.log(t('card.search.empty', { query }));
489
+ return;
490
+ }
491
+ console.log(t('card.search.results', { query, count: cards.length }));
492
+ if (filters?.page !== undefined || filters?.limit !== undefined) {
493
+ const page = (filters.page ?? 0) + 1;
494
+ const limit = filters.limit ?? 50;
495
+ console.log(t('card.search.pagination', { page, limit }));
496
+ }
497
+ this.outputFormatter.output(cards, {
498
+ fields: ['name', 'id', 'url'],
499
+ headers: ['Name', 'ID', 'URL'],
500
+ });
501
+ }
544
502
  }
@@ -387,6 +387,31 @@ export class CommandController {
387
387
  }, 'cards show');
388
388
  });
389
389
 
390
+ cardsCmd
391
+ .command('search <query>')
392
+ .description(t('commands.cards.search.description'))
393
+ .option('-f, --format <format>', t('commands.formatOption'), 'table')
394
+ .option('--board-id <boardId>', t('commands.cards.search.boardIdOption'))
395
+ .option('--list-id <listId>', t('commands.cards.search.listIdOption'))
396
+ .option('--labels <labels>', t('commands.cards.search.labelsOption'))
397
+ .option('--limit <limit>', t('commands.cards.search.limitOption'))
398
+ .option('--page <page>', t('commands.cards.search.pageOption'))
399
+ .action(async (query: string, options: { format?: string; boardId?: string; listId?: string; labels?: string; limit?: string; page?: string }) => {
400
+ await ErrorHandler.withErrorHandling(async () => {
401
+ await this.initializeTrelloControllers();
402
+ if (options.format) {
403
+ this.outputFormatter.setFormat(options.format as OutputFormat);
404
+ }
405
+ await this.cardController.searchCards(query, {
406
+ boardId: options.boardId,
407
+ listId: options.listId,
408
+ labels: options.labels,
409
+ limit: options.limit !== undefined ? Number.parseInt(options.limit, 10) : undefined,
410
+ page: options.page !== undefined ? Number.parseInt(options.page, 10) : undefined,
411
+ });
412
+ }, 'cards search');
413
+ });
414
+
390
415
  // Backward compatibility subcommand for legacy behavior
391
416
  cardsCmd
392
417
  .command('legacy <boardName> <listName>')