@microteia/console 2.28.1 → 2.29.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.
@@ -22,6 +22,10 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.29.0] — 2026-10-15
26
+
27
+ - feat(export-catalog): each installed extension now carries `navGroups` — the sidebar groups it declares, by key, with the name resolved in every language the package ships and its order. The authoring tools list them in `microteia://nav-groups`, so an extension joins an existing group instead of drawing a second one with the same title. Always written, even empty; a snapshot taken by an older Console is read as "groups unknown".
28
+
25
29
  #### [2.28.1] — 2026-10-14
26
30
 
27
31
  - fix(console/deploy): a function whose source declares `retry: true` is no longer refused on its first deploy. Without a terminal the Firebase CLI does not ask "Would you like to proceed with deployment?" — it aborts with "Pass the --force option to deploy functions with a failure policy", so `runAggregateBackfill` and `runRecordKeyAdoption` never went up through `microteia deploy` or `provision`. That one function is now redeployed with `--force`, and only when the project is known to hold no copy of it in another region: the same flag would delete that copy without asking, which stays a `--prune` decision. When a copy stands or the project could not be listed, the refusal is terminal and the hint points to `microteia deploy --prune`.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.28.1
1
+ # Microteia Console 2.29.0
2
2
 
3
3
  Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
4
4
  what it needs are command-line tools that are already on the machine, listed at the bottom.
@@ -98,6 +98,20 @@ export interface CatalogExtensionExportedArtifact {
98
98
  kind: 'workflow' | 'action' | 'script' | 'unknown';
99
99
  name: string;
100
100
  }
101
+ /**
102
+ * Um grupo da barra lateral que a extensão declara (`ui.nav.group`). Viaja porque a CHAVE é contrato:
103
+ * é o que outra extensão escreve para entrar no grupo, e quem autora sem enxergá-la inventa outra — e
104
+ * o menu passa a mostrar dois grupos com o mesmo nome.
105
+ *
106
+ * O nome vai resolvido em cada idioma que o próprio pacote traz, porque é pelo nome que o autor
107
+ * reconhece o grupo; o rótulo literal vai como está, com `translations` vazio.
108
+ */
109
+ export interface CatalogExtensionNavGroup {
110
+ key: string;
111
+ label: string;
112
+ translations: Record<string, string>;
113
+ order: number;
114
+ }
101
115
  export interface CatalogExtensionExport {
102
116
  id: string;
103
117
  name: string;
@@ -108,6 +122,8 @@ export interface CatalogExtensionExport {
108
122
  * a chave ausente é o silêncio de um Console anterior a este campo, e o kernel distingue as duas.
109
123
  */
110
124
  exports: CatalogExtensionExportedArtifact[];
125
+ /** Sempre escrita, mesmo vazia, pela mesma razão de `exports`. */
126
+ navGroups: CatalogExtensionNavGroup[];
111
127
  }
112
128
  export interface CatalogExport {
113
129
  version: number;
@@ -78,6 +78,54 @@ const exportedArtifacts = (extension) => {
78
78
  .map((id) => named.get(id) ?? { id, kind: 'unknown', name: id })
79
79
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
80
80
  };
81
+ const NAV_GROUP_POINT = 'ui.nav.group';
82
+ const MESSAGES_POINT = 'i18n.messages';
83
+ const DEFAULT_LOCALE = 'en';
84
+ const MESSAGE_REFERENCE = /^@([a-z0-9.-]+)\/(.+)$/;
85
+ /**
86
+ * Os grupos da barra lateral de uma cópia instalada, com o nome resolvido pelos `i18n.messages` do
87
+ * próprio manifesto. É a mesma leitura que `declaredNavGroupsOf` faz no kernel, repetida porque o
88
+ * Console não alcança `src/` — e é o teste que atravessa a fronteira que confere as duas.
89
+ */
90
+ const navGroupsOf = (extension) => {
91
+ const contributes = Array.isArray(extension.manifest?.contributes) ? extension.manifest.contributes : [];
92
+ const contributions = contributes.map(asRecord).filter((one) => one !== undefined);
93
+ const bundles = contributions
94
+ .filter((one) => one.point === MESSAGES_POINT)
95
+ .map((one) => asRecord(one.metadata))
96
+ .filter((one) => one !== undefined);
97
+ const translationsOf = (label) => {
98
+ const match = MESSAGE_REFERENCE.exec(label);
99
+ if (!match)
100
+ return {};
101
+ const found = {};
102
+ for (const bundle of bundles) {
103
+ const locale = text(bundle.locale);
104
+ const message = asRecord(bundle.messages)?.[match[2]];
105
+ if (bundle.namespace !== match[1] || locale === undefined || typeof message !== 'string' || message.trim() === '')
106
+ continue;
107
+ if (found[locale] === undefined)
108
+ found[locale] = message;
109
+ }
110
+ return found;
111
+ };
112
+ return contributions
113
+ .filter((one) => one.point === NAV_GROUP_POINT)
114
+ .flatMap((one) => {
115
+ const metadata = asRecord(one.metadata) ?? {};
116
+ const key = text(metadata.key)?.trim();
117
+ if (key === undefined)
118
+ return [];
119
+ const raw = text(metadata.label) ?? key;
120
+ const translations = translationsOf(raw);
121
+ return [{
122
+ key,
123
+ label: translations[DEFAULT_LOCALE] ?? Object.values(translations)[0] ?? raw,
124
+ translations,
125
+ order: typeof metadata.order === 'number' ? metadata.order : 0,
126
+ }];
127
+ });
128
+ };
81
129
  /**
82
130
  * A mesma dedução que a aplicação faz ao ler a tela (`screenDefinitionMapper.ts`): o `origin` gravado
83
131
  * quando ele existe, senão `plugin` para quem tem dono e `design` para o que o escritório criou. Ela é
@@ -174,6 +222,7 @@ export const buildCatalogExport = (input) => ({
174
222
  version: one.version,
175
223
  active: one.active,
176
224
  exports: exportedArtifacts(one),
225
+ navGroups: navGroupsOf(one),
177
226
  }))
178
227
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
179
228
  screens: input.screens
@@ -293,6 +342,8 @@ export const exportCatalog = async (args) => {
293
342
  + `the rest the office built itself.`);
294
343
  const exported = document.extensions.reduce((total, one) => total + one.exports.length, 0);
295
344
  info(` ${exported} artifact(s) are declared for other extensions to reference as <extension-id>/<id>.`);
345
+ const groups = document.extensions.reduce((total, one) => total + one.navGroups.length, 0);
346
+ info(` ${groups} sidebar group(s) are declared, by key, for other extensions to join.`);
296
347
  blank();
297
348
  await checkIgnored(directory);
298
349
  info('It is a SNAPSHOT, and it says so: the authoring tools report when it was taken and warn when it');
@@ -87,4 +87,4 @@ export declare const consoleRoot: string;
87
87
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
88
88
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
89
89
  */
90
- export declare const CLI_VERSION = "2.28.1";
90
+ export declare const CLI_VERSION = "2.29.0";
@@ -94,4 +94,4 @@ export const consoleRoot = resolve(here, '..', '..');
94
94
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
95
95
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
96
96
  */
97
- export const CLI_VERSION = '2.28.1';
97
+ export const CLI_VERSION = '2.29.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microteia/console",
3
- "version": "2.28.1",
3
+ "version": "2.29.0",
4
4
  "description": "Install and update Microteia in a Firebase project you own.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",