@microteia/console 2.26.0 → 2.28.1

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,23 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.28.1] — 2026-10-14
26
+
27
+ - 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`.
28
+ - refactor(console/regionOrphans): `clearRegionOrphans` returns the names whose old-region copy is still standing (`null` when the project could not be listed) instead of a boolean.
29
+
30
+ #### [2.28.0] — 2026-10-13
31
+
32
+ - feat(examples): the Console ships `business-days` as authoring source — a collection the kernel reads without knowing its name (`time.business.calendar`), and a code view that edits data no field type describes.
33
+
34
+ #### [2.27.0] — 2026-10-13
35
+
36
+ - feat(examples): the Console ships `document-example` as authoring source — a `document.template` contribution (Markdown, with `{{#each}}`/`{{#if}}` and a child-record `dataset`) over two screens addressed by `sourceId`.
37
+
38
+ #### [2.26.1] — 2026-10-12
39
+
40
+ - fix(connect): the "enable the Firestore TTL policy on auditLogs.expiresAt" prerequisite is gone from `connect` and from `MICROTEIA.md`. The policy is declared — `"ttl": true` in a `fieldOverride` of `firestore.indexes.json` — and the `firebase deploy --only firestore:indexes` that `deploy` runs applies it; the step asked the organization to redo by hand what the deploy had already done. The same holds for every TTL the kernel declares (`workflow_trigger_marks.expiresAt` too).
41
+
25
42
  #### [2.26.0] — 2026-10-09
26
43
 
27
44
  - feat(plugin-init): the organization's namespace is required — `--namespace com.acme`, or asked for when there is a terminal — validated before anything is written, and recorded in the seed `package.json` under `microteia.namespace`. `--force` reuses the one already recorded. The SDK derives every extension id in the directory from it.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.26.0
1
+ # Microteia Console 2.28.1
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.
@@ -185,7 +185,7 @@ export const deploy = async (args) => {
185
185
  // ANTES de deployar: uma cópia da mesma function numa região que o pacote abandonou faz o
186
186
  // `firebase` recusar o deploy dela, tentativa após tentativa, e faz um gatilho de Firestore
187
187
  // disparar duas vezes por gravação enquanto ninguém percebe.
188
- await clearRegionOrphans(projectId, own, region, hasFlag(args, 'prune'), deployedNow);
188
+ const standingCopies = await clearRegionOrphans(projectId, own, region, hasFlag(args, 'prune'), deployedNow);
189
189
  // Numa instalação nova não há carimbo nenhum com que comparar, e o mecanismo cai sozinho no
190
190
  // caminho de sempre — não é preciso um desvio para isso. `--all` é o pedido explícito de subir
191
191
  // tudo, para a configuração trocada por fora, que carimbo nenhum enxerga.
@@ -194,6 +194,7 @@ export const deploy = async (args) => {
194
194
  cwd: install,
195
195
  delaySeconds,
196
196
  force: firstInstall,
197
+ standingCopies,
197
198
  }, { deployed: deployedNow, region, all: hasFlag(args, 'all') });
198
199
  refreshedStamps = report.refreshed;
199
200
  skippedFunctions = report.skipped.length;
@@ -4,6 +4,11 @@ export interface FunctionDeployOptions {
4
4
  delaySeconds: number;
5
5
  /** `--force` deixa o Firebase configurar sozinho a política de limpeza do Artifact Registry. */
6
6
  force: boolean;
7
+ /**
8
+ * As functions que ainda têm cópia numa região abandonada, como `clearRegionOrphans` devolve;
9
+ * `null` quando não deu para saber. Só fora dele a recusa da política de repetição é contornada.
10
+ */
11
+ standingCopies: ReadonlySet<string> | null;
7
12
  }
8
13
  export interface FunctionDeployFailure {
9
14
  name: string;
@@ -39,7 +44,20 @@ export interface TerminalFailure {
39
44
  match: RegExp;
40
45
  hint: string[];
41
46
  }
47
+ /**
48
+ * A recusa do `firebase` sem terminal diante de uma function que passa a ter `retry`.
49
+ *
50
+ * Com terminal ele pergunta "Would you like to proceed with deployment?"; sem, recusa e pede
51
+ * `--force`. A pergunta é sobre o que o PRÓPRIO código declarou (`retry: true` no gatilho), e quem
52
+ * declarou já respondeu: repetir a entrega é o que torna o gatilho confiável. Por isso a recusa é
53
+ * contornada com `--force` naquela function, e só nela — e só quando não há cópia dela em outra
54
+ * região, porque o mesmo `--force` faria o `firebase` apagar essa cópia sem perguntar.
55
+ *
56
+ * A pergunta só existe na primeira vez: com a function no ar já com `retry`, ela não volta.
57
+ */
58
+ export declare const FAILURE_POLICY_REFUSAL: RegExp;
42
59
  export declare const terminalFailure: (output: string) => TerminalFailure | undefined;
60
+ export declare const mayConfirmRetryPolicy: (name: string, output: string, options: FunctionDeployOptions) => boolean;
43
61
  export declare const deployFunctionsOneByOne: (names: string[], options: FunctionDeployOptions) => Promise<FunctionDeployReport>;
44
62
  export declare const reportFailures: (failures: FunctionDeployFailure[]) => void;
45
63
  export declare const reportNotices: (notices: string[]) => void;
@@ -64,6 +64,18 @@ export const failureReason = (output) => {
64
64
  ?? 'no output from firebase';
65
65
  return chosen.length > REASON_LIMIT ? `${chosen.slice(0, REASON_LIMIT - 1)}…` : chosen;
66
66
  };
67
+ /**
68
+ * A recusa do `firebase` sem terminal diante de uma function que passa a ter `retry`.
69
+ *
70
+ * Com terminal ele pergunta "Would you like to proceed with deployment?"; sem, recusa e pede
71
+ * `--force`. A pergunta é sobre o que o PRÓPRIO código declarou (`retry: true` no gatilho), e quem
72
+ * declarou já respondeu: repetir a entrega é o que torna o gatilho confiável. Por isso a recusa é
73
+ * contornada com `--force` naquela function, e só nela — e só quando não há cópia dela em outra
74
+ * região, porque o mesmo `--force` faria o `firebase` apagar essa cópia sem perguntar.
75
+ *
76
+ * A pergunta só existe na primeira vez: com a function no ar já com `retry`, ela não volta.
77
+ */
78
+ export const FAILURE_POLICY_REFUSAL = /Pass the --force option to deploy functions with a failure policy/;
67
79
  const TERMINAL_FAILURES = [
68
80
  {
69
81
  match: /Aborting because deletion cannot proceed in non-interactive mode/,
@@ -74,6 +86,15 @@ const TERMINAL_FAILURES = [
74
86
  ' microteia deploy --prune',
75
87
  ],
76
88
  },
89
+ {
90
+ match: FAILURE_POLICY_REFUSAL,
91
+ hint: [
92
+ 'The function now retries failed deliveries, and the Firebase CLI asks to confirm that once.',
93
+ 'It was not confirmed here because the project may still hold a copy of the function in another',
94
+ 'region, which the same confirmation would delete without asking. Remove old copies first:',
95
+ ' microteia deploy --prune',
96
+ ],
97
+ },
77
98
  {
78
99
  match: /does not have a default and cannot be answered in non-interactive mode/,
79
100
  hint: [
@@ -97,9 +118,9 @@ export const terminalFailure = (output) => {
97
118
  * gatilho falha enquanto as callables sobem normalmente. Repetir resolve, e sem isso a primeira
98
119
  * instalação parece quebrada quando está apenas incompleta.
99
120
  */
100
- const deployOne = async (name, options) => {
121
+ const deployOne = async (name, options, force = options.force) => {
101
122
  const args = ['deploy', '--only', `functions:${name}`, '--project', options.projectId];
102
- if (options.force)
123
+ if (force)
103
124
  args.push('--force');
104
125
  const startedAt = Date.now();
105
126
  const result = await capture(FIREBASE_BIN, args, { cwd: options.cwd });
@@ -111,6 +132,10 @@ const deployOne = async (name, options) => {
111
132
  seconds: Math.round((Date.now() - startedAt) / 1000),
112
133
  };
113
134
  };
135
+ export const mayConfirmRetryPolicy = (name, output, options) => !options.force
136
+ && options.standingCopies !== null
137
+ && !options.standingCopies.has(name)
138
+ && FAILURE_POLICY_REFUSAL.test(stripVTControlCharacters(output));
114
139
  export const deployFunctionsOneByOne = async (names, options) => {
115
140
  const deployed = [];
116
141
  const failed = [];
@@ -134,9 +159,15 @@ export const deployFunctionsOneByOne = async (names, options) => {
134
159
  const name = remaining[index];
135
160
  const label = `[${index + 1}/${remaining.length}] ${name}`;
136
161
  pending(label);
137
- const { ok, output, notices: given, seconds } = await deployOne(name, options);
162
+ let { ok, output, notices: given, seconds } = await deployOne(name, options);
138
163
  for (const notice of given)
139
164
  notices.add(notice);
165
+ if (!ok && mayConfirmRetryPolicy(name, output, options)) {
166
+ info(` ${name} now retries failed deliveries, as its source declares: confirming it`);
167
+ ({ ok, output, notices: given, seconds } = await deployOne(name, options, true));
168
+ for (const notice of given)
169
+ notices.add(notice);
170
+ }
140
171
  if (ok) {
141
172
  success(`${label} (${seconds}s)`);
142
173
  deployed.push(name);
@@ -428,8 +428,8 @@ export const provisionBackend = async (options) => {
428
428
  }
429
429
  // A extensão que mudou de região deixa a cópia antiga para trás igual ao kernel, e ali o efeito
430
430
  // é o mesmo: deploy recusado em toda tentativa, e gatilho disparando em duas regiões.
431
- await clearRegionOrphans(projectId, toDeploy, region, options.prune, await projectFunctions());
432
- const deployOptions = { projectId, cwd: install, delaySeconds, force: false };
431
+ const standingCopies = await clearRegionOrphans(projectId, toDeploy, region, options.prune, await projectFunctions());
432
+ const deployOptions = { projectId, cwd: install, delaySeconds, force: false, standingCopies };
433
433
  if (!probing)
434
434
  info(` deploying ${toDeploy.length} function(s), one at a time…`);
435
435
  const report = probing
@@ -50,10 +50,15 @@ export declare const deleteCommand: (projectId: string, orphan: RegionOrphan) =>
50
50
  * Avisa por padrão e só apaga com `--prune`, como `settleOrphans` faz com a function que perdeu dono.
51
51
  * Apagar uma Cloud Function é irreversível e é do escritório, não nosso: o comando tem de ser pedido.
52
52
  */
53
- export declare const settleRegionOrphans: (projectId: string, orphans: RegionOrphan[], prune: boolean) => Promise<boolean>;
53
+ export declare const settleRegionOrphans: (projectId: string, orphans: RegionOrphan[], prune: boolean) => Promise<ReadonlySet<string>>;
54
54
  /**
55
- * O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve `true` quando o caminho está
56
- * livre — inclusive quando não deu para verificar, pela mesma razão de `listDeployedFunctions`.
55
+ * O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve os nomes cuja cópia antiga
56
+ * CONTINUA de pé, ou `null` quando não deu para verificar — e não bloqueia o deploy em nenhum dos dois
57
+ * casos, pela mesma razão de `listDeployedFunctions`.
58
+ *
59
+ * O conjunto é o que autoriza o `--force` pontual de `deployFunctionsOneByOne`: com ele o `firebase`
60
+ * aceita a política de repetição E apaga sem perguntar a cópia de outra região, e essa segunda decisão
61
+ * é do escritório, pedida com `--prune`. Não saber equivale a ter cópia.
57
62
  */
58
63
  export declare const clearRegionOrphans: (projectId: string, wanted: string[], region: string, prune: boolean,
59
64
  /**
@@ -61,4 +66,4 @@ export declare const clearRegionOrphans: (projectId: string, wanted: string[], r
61
66
  * a mesma coisa ao mesmo projeto não é caro, é ambíguo: as duas respostas podem divergir, e a
62
67
  * decisão passa a depender de qual delas o código olhou.
63
68
  */
64
- listed?: DeployedFunction[] | null) => Promise<boolean>;
69
+ listed?: DeployedFunction[] | null) => Promise<ReadonlySet<string> | null>;
@@ -51,7 +51,7 @@ export const deleteCommand = (projectId, orphan) => `firebase functions:delete $
51
51
  */
52
52
  export const settleRegionOrphans = async (projectId, orphans, prune) => {
53
53
  if (orphans.length === 0)
54
- return true;
54
+ return new Set();
55
55
  blank();
56
56
  warn(`${orphans.length} function(s) are still deployed in a region this package no longer uses:`);
57
57
  bullets(orphans.map((orphan) => `${orphan.name} (${orphan.region})`));
@@ -66,11 +66,11 @@ export const settleRegionOrphans = async (projectId, orphans, prune) => {
66
66
  info(' or one at a time:');
67
67
  bullets(orphans.map((orphan) => deleteCommand(projectId, orphan)));
68
68
  blank();
69
- return false;
69
+ return new Set(orphans.map((orphan) => orphan.name));
70
70
  }
71
71
  const plural = orphans.length === 1 ? 'copy' : 'copies';
72
72
  info(` --prune: removing the ${orphans.length} old ${plural} before deploying.`);
73
- let cleared = true;
73
+ const standing = new Set();
74
74
  for (const orphan of orphans) {
75
75
  const label = `${orphan.name} (${orphan.region})`;
76
76
  pending(label);
@@ -87,15 +87,20 @@ export const settleRegionOrphans = async (projectId, orphans, prune) => {
87
87
  failure(`${label} could not be removed`);
88
88
  info(` ${failureReason(`${result.stdout}${result.stderr}`)}`);
89
89
  info(` ${deleteCommand(projectId, orphan)}`);
90
- cleared = false;
90
+ standing.add(orphan.name);
91
91
  }
92
92
  }
93
93
  blank();
94
- return cleared;
94
+ return standing;
95
95
  };
96
96
  /**
97
- * O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve `true` quando o caminho está
98
- * livre — inclusive quando não deu para verificar, pela mesma razão de `listDeployedFunctions`.
97
+ * O passo inteiro, para quem só quer chamá-lo antes de deployar. Devolve os nomes cuja cópia antiga
98
+ * CONTINUA de pé, ou `null` quando não deu para verificar — e não bloqueia o deploy em nenhum dos dois
99
+ * casos, pela mesma razão de `listDeployedFunctions`.
100
+ *
101
+ * O conjunto é o que autoriza o `--force` pontual de `deployFunctionsOneByOne`: com ele o `firebase`
102
+ * aceita a política de repetição E apaga sem perguntar a cópia de outra região, e essa segunda decisão
103
+ * é do escritório, pedida com `--prune`. Não saber equivale a ter cópia.
99
104
  */
100
105
  export const clearRegionOrphans = async (projectId, wanted, region, prune,
101
106
  /**
@@ -106,6 +111,6 @@ export const clearRegionOrphans = async (projectId, wanted, region, prune,
106
111
  listed) => {
107
112
  const deployed = listed === undefined ? await listDeployedFunctions(projectId) : listed;
108
113
  if (deployed === null)
109
- return true;
114
+ return null;
110
115
  return settleRegionOrphans(projectId, regionOrphans(deployed, wanted, region), prune);
111
116
  };
@@ -91,13 +91,6 @@ export const MANUAL_PREREQUISITES = [
91
91
  + 'with the credential it was given and refuses everything else.',
92
92
  optional: true,
93
93
  },
94
- {
95
- id: 'audit-ttl',
96
- title: 'Enable the Firestore TTL policy on auditLogs.expiresAt',
97
- detail: 'Firestore console, TTL, collection auditLogs, field expiresAt. It cannot be declared in code or '
98
- + 'rules. Without it audit logs are never expired and grow without bound.',
99
- optional: false,
100
- },
101
94
  ];
102
95
  /**
103
96
  * Os nomes das Cloud Functions saem de `functions/src/index.ts`, a mesma fonte e o mesmo parse que
@@ -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.26.0";
90
+ export declare const CLI_VERSION = "2.28.1";
@@ -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.26.0';
97
+ export const CLI_VERSION = '2.28.1';
@@ -35,6 +35,7 @@ export const EXAMPLE_SOURCES = {
35
35
  agent: 'agent.ts',
36
36
  brasil: 'brasil.ts',
37
37
  calendar: 'calendar.ts',
38
+ 'business-days': 'businessDays.ts',
38
39
  dashboard: 'dashboard.ts',
39
40
  'locale-pt-br': 'localePtBr',
40
41
  news: 'news.ts',
@@ -50,6 +51,7 @@ export const EXAMPLE_SOURCES = {
50
51
  'command-example': 'examples/command-example',
51
52
  'settings-example': 'examples/settings-example',
52
53
  'record-tab-example': 'examples/record-tab-example',
54
+ 'document-example': 'examples/document-example',
53
55
  };
54
56
  /**
55
57
  * Os exemplos que existem SÓ para ser lidos — os que não estão no catálogo embutido.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microteia/console",
3
- "version": "2.26.0",
3
+ "version": "2.28.1",
4
4
  "description": "Install and update Microteia in a Firebase project you own.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",