@microteia/console 2.20.0 → 2.22.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,49 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.22.1] — 2026-09-19
26
+
27
+ - fix(cli/publish): the release notes now carry every changelog entry since the last PUBLISHED version,
28
+ not only the one being packaged. A version can be packaged and never published — the catalog is what
29
+ says what went up, and the package being published contains all of them. Publishing 9.66.0 over a
30
+ catalog whose newest entry was 9.64.0 created a release that carries 9.65.0 and did not quote a line
31
+ of it, and a release is immutable: no later release goes back to tell what those versions changed.
32
+ The cut now starts at `publishedBefore(index, version)`, so it happens after the catalog is read
33
+ rather than while the package is; the first publish, which has no interval, still cuts its own entry
34
+ alone. The command prints which versions the notes carry, in the dry run too.
35
+
36
+ #### [2.22.0] — 2026-09-10
37
+
38
+ - feat(deploy): the reachability check now covers HTTP endpoints, not only callables.
39
+ `callableFunctionNames` matches `onRequest` as well, because the list means "what the outside world
40
+ calls" and an HTTP endpoint depends on public invocation even more than a callable does — a blocked
41
+ invoker grant answers 403 with no CORS header, and the client reports a connection error. Without
42
+ this, the platform's first `onRequest` would have gone up unverified.
43
+ - fix(deploy): the `functions-invoker` prerequisite no longer promises that every reachable function
44
+ rejects a caller with no signed-in user. That is true of a callable and false of an endpoint that
45
+ authenticates by token, and the sentence was about to be read by whoever installs the agent access.
46
+
47
+ #### [2.21.0] — 2026-09-09
48
+
49
+ - fix(cli/pack): reading-only examples are left out of the first-party backend accounting.
50
+ `extension-templates/` holds two kinds of package with the same shape and different owners — the
51
+ first-party extensions, whose Cloud Functions this kernel compiles and deploys, and the examples,
52
+ whose backend belongs to whoever installs them. The pack derives its baseline as
53
+ `all functions − claimed`, so the first example to teach Cloud Functions would have aborted every
54
+ publish by claiming a name that is not here, and a name that WAS here would have been taken out of
55
+ the kernel baseline by a mere template. `firstPartyTemplates` is now the single door onto that
56
+ directory, and `READING_ONLY_EXAMPLES` is derived from where an example's source lives rather than
57
+ kept as a second list.
58
+ - feat(cli/vendor): `stageExamples` carries `functions/` and `scripts/` beside `views/`, and an
59
+ example whose source is a DIRECTORY takes `plugin.ts` as its entry. Without both, an example could
60
+ not teach a Cloud Function or a sandbox script — the two files simply would not travel.
61
+ - fix(cli/vendor): `rewriteImports` sends the node-only modules to `@microteia/sdk/node` instead of to
62
+ the isomorphic barrel, which is where `transpileScriptSource` deliberately is not. The symptom
63
+ would have been in the office of whoever installed, not here.
64
+ - fix(cli/vendor): the `MICROTEIA.md` it writes derives its build command from the examples actually
65
+ present. It named `examples/pessoa`, which left the repository with the office extensions — a
66
+ generated document telling the reader to compile something that is not there.
67
+
25
68
  #### [2.20.0] — 2026-09-01
26
69
 
27
70
  - feat(cli/export-catalog): the snapshot carries what each installed extension declares in `manifest.exports` — the artifacts another extension may reference as `<extension-id>/<source-id>`. Whoever authors outside the office knew an extension was installed and had no way to learn the name of anything inside it, so the reference could be written by hand and never discovered. Each entry carries the id, the artifact's name, and its `kind`, which is the part that decides where the reference fits: a script goes in `scriptId`, an automation in `targetWorkflowId`. What the artifact DOES still never travels — the steps and the code are behaviour, and the snapshot is shape.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.20.0
1
+ # Microteia Console 2.22.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.
@@ -70,6 +70,17 @@ export declare const entryOf: (manifest: KernelManifest, urls: {
70
70
  * `9.8.2` significa. Um número novo custa um minuto.
71
71
  */
72
72
  export declare const withEntry: (index: ChannelIndex, entry: ChannelEntry) => ChannelIndex;
73
+ /**
74
+ * A última versão publicada ABAIXO desta — o começo do intervalo que o release novo cobre.
75
+ *
76
+ * É "abaixo desta" e não "o topo do catálogo" porque publicar fora de ordem é possível, e o que o
77
+ * release precisa descrever é o que ele acrescenta a quem já tinha a anterior. Quem já tinha a anterior
78
+ * está na maior versão MENOR que esta.
79
+ *
80
+ * Nada publicado ainda é `undefined`, e não a menor versão do histórico: o arquivo de histórico começa
81
+ * antes do catálogo, e a primeira publicação não descreve o que veio antes dela.
82
+ */
83
+ export declare const publishedBefore: (index: ChannelIndex, version: string) => string | undefined;
73
84
  /**
74
85
  * Resolve o que foi pedido — `latest` ou um número — nomeando o que existe quando não acha.
75
86
  *
@@ -126,6 +126,20 @@ export const withEntry = (index, entry) => {
126
126
  kernel: [entry, ...index.kernel].sort(byVersionDescending),
127
127
  };
128
128
  };
129
+ /**
130
+ * A última versão publicada ABAIXO desta — o começo do intervalo que o release novo cobre.
131
+ *
132
+ * É "abaixo desta" e não "o topo do catálogo" porque publicar fora de ordem é possível, e o que o
133
+ * release precisa descrever é o que ele acrescenta a quem já tinha a anterior. Quem já tinha a anterior
134
+ * está na maior versão MENOR que esta.
135
+ *
136
+ * Nada publicado ainda é `undefined`, e não a menor versão do histórico: o arquivo de histórico começa
137
+ * antes do catálogo, e a primeira publicação não descreve o que veio antes dela.
138
+ */
139
+ export const publishedBefore = (index, version) => [...index.kernel]
140
+ .sort(byVersionDescending)
141
+ .find((entry) => compareVersions(entry.version, version) < 0)
142
+ ?.version;
129
143
  /**
130
144
  * Resolve o que foi pedido — `latest` ou um número — nomeando o que existe quando não acha.
131
145
  *
@@ -127,10 +127,20 @@ export declare const MANUAL_PREREQUISITES: KernelPrerequisite[];
127
127
  */
128
128
  export declare const exportedFunctionNames: (indexSource: string) => string[];
129
129
  /**
130
- * As callables são as functions que o NAVEGADOR chama. Elas se distinguem das de gatilho por uma
131
- * razão prática: só elas podem ser verificadas de fora, e só nelas a permissão de invocação pública
132
- * importa. Sem ela a aplicação abre e toda chamada ao servidor falha — parecendo erro de CORS, que
133
- * manda procurar no lugar errado.
130
+ * As functions que o mundo de FORA chama — e é isso que a lista significa, não o construtor delas.
131
+ *
132
+ * Elas se distinguem das de gatilho por uma razão prática: só elas podem ser verificadas de fora, e
133
+ * só nelas a permissão de invocação pública importa. Sem ela a aplicação abre e toda chamada ao
134
+ * servidor falha — parecendo erro de CORS, que manda procurar no lugar errado.
135
+ *
136
+ * `onRequest` entra pela MESMA razão, e não por generosidade: um endpoint HTTP é ainda mais
137
+ * dependente da invocação pública que uma callable, porque quem o consome pode não ser um navegador
138
+ * nosso — é o caso do endpoint MCP, que um cliente de IA chama. O preflight de `callableAccess`
139
+ * serve os dois sem mudar uma linha: ele pergunta `OPTIONS` e confere o cabeçalho de CORS, que um
140
+ * `onRequest` com `cors: true` responde igual.
141
+ *
142
+ * O que NÃO se confunde com isto é o portão de instalação (`functionGate`): lá a distinção entre
143
+ * `onCall` e `onRequest` importa muito, porque uma tem escritório no chamador e a outra não.
134
144
  */
135
145
  export declare const callableFunctionNames: (source: string) => string[];
136
146
  /**
@@ -86,7 +86,9 @@ export const MANUAL_PREREQUISITES = [
86
86
  + 'CORS error. The fix is one command per function, and the deploy prints the exact list: gcloud '
87
87
  + 'run services add-iam-policy-binding <function-name-in-lowercase> --region=<region> '
88
88
  + '--project=<projectId> --member=allUsers --role=roles/run.invoker. Unauthenticated invocation '
89
- + 'is not open access: every callable still rejects callers with no signed-in user.',
89
+ + 'is not open access: reaching a function is not the same as being allowed to use it. A callable '
90
+ + 'rejects any caller with no signed-in user, and an HTTP endpoint authenticates every request '
91
+ + 'with the credential it was given and refuses everything else.',
90
92
  optional: true,
91
93
  },
92
94
  {
@@ -107,12 +109,23 @@ export const exportedFunctionNames = (indexSource) => [...indexSource.matchAll(/
107
109
  .map((name) => name.trim())
108
110
  .filter((name) => name !== '');
109
111
  /**
110
- * As callables são as functions que o NAVEGADOR chama. Elas se distinguem das de gatilho por uma
111
- * razão prática: só elas podem ser verificadas de fora, e só nelas a permissão de invocação pública
112
- * importa. Sem ela a aplicação abre e toda chamada ao servidor falha — parecendo erro de CORS, que
113
- * manda procurar no lugar errado.
112
+ * As functions que o mundo de FORA chama — e é isso que a lista significa, não o construtor delas.
113
+ *
114
+ * Elas se distinguem das de gatilho por uma razão prática: só elas podem ser verificadas de fora, e
115
+ * só nelas a permissão de invocação pública importa. Sem ela a aplicação abre e toda chamada ao
116
+ * servidor falha — parecendo erro de CORS, que manda procurar no lugar errado.
117
+ *
118
+ * `onRequest` entra pela MESMA razão, e não por generosidade: um endpoint HTTP é ainda mais
119
+ * dependente da invocação pública que uma callable, porque quem o consome pode não ser um navegador
120
+ * nosso — é o caso do endpoint MCP, que um cliente de IA chama. O preflight de `callableAccess`
121
+ * serve os dois sem mudar uma linha: ele pergunta `OPTIONS` e confere o cabeçalho de CORS, que um
122
+ * `onRequest` com `cors: true` responde igual.
123
+ *
124
+ * O que NÃO se confunde com isto é o portão de instalação (`functionGate`): lá a distinção entre
125
+ * `onCall` e `onRequest` importa muito, porque uma tem escritório no chamador e a outra não.
114
126
  */
115
- export const callableFunctionNames = (source) => [...source.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*onCall\b/g)].map((match) => match[1]);
127
+ export const callableFunctionNames = (source) => [...source.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:onCall|onRequest)\b/g)]
128
+ .map((match) => match[1]);
116
129
  /**
117
130
  * Os parâmetros que o backend declara com `defineString`. O `firebase-functions` os valida
118
131
  * GLOBALMENTE, e não por function: um parâmetro sem valor e sem default recusa o deploy de TODAS as
@@ -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.20.0";
90
+ export declare const CLI_VERSION = "2.22.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.20.0';
97
+ export const CLI_VERSION = '2.22.1';
@@ -38,16 +38,28 @@ export declare const changelogProblems: (file: string, contents: string, version
38
38
  */
39
39
  export declare const readChangelog: (directory: string, file: string, version: string) => Promise<string>;
40
40
  /**
41
- * A entrada de UMA versão, para ser o corpo do release publicado.
41
+ * As entradas que o release publicado carrega: a da versão que está saindo, e a de TODA versão entre ela
42
+ * e a última já publicada.
42
43
  *
43
44
  * O texto do release não é escrito à mão em lugar nenhum: ele é o mesmo parágrafo que já foi conferido
44
45
  * pelo `pack`, recortado daqui. Dois textos sobre a mesma versão divergem — e o que divergiria é
45
46
  * justamente o que fica visível para quem procura o que mudou.
46
47
  *
47
- * Recorta do cabeçalho da versão até o próximo cabeçalho, e não até o fim: o que vem depois é o
48
- * histórico anterior, e ele já está publicado nos releases dele.
48
+ * O recorte de uma entrada só perdia o intervalo. Publicar a 9.66.0 quando a última publicada é a
49
+ * 9.64.0 — porque a 9.65.0 foi empacotada e não subiu — entrega um release que CONTÉM a 9.65.0 e não
50
+ * menciona uma linha dela: para quem procura o que mudou, aquelas alterações nunca existiram, e não há
51
+ * release posterior que as traga de volta. O release descreve o que ele leva, e ele leva tudo desde a
52
+ * última publicação.
53
+ *
54
+ * Sem `since` é a primeira publicação, e aí o recorte é só da própria versão: o histórico do arquivo é
55
+ * anterior ao catálogo, e despejá-lo inteiro não seria descrever o que subiu.
49
56
  */
50
- export declare const changelogEntry: (contents: string, version: string) => string;
57
+ export interface ChangelogNotes {
58
+ body: string;
59
+ /** As versões recortadas, da mais recente para a mais antiga — o que o release diz que cobre. */
60
+ versions: string[];
61
+ }
62
+ export declare const changelogEntries: (contents: string, version: string, since?: string) => ChangelogNotes;
51
63
  export interface ChangelogRefresh {
52
64
  written: string[];
53
65
  missing: string[];
@@ -83,26 +83,25 @@ export const readChangelog = async (directory, file, version) => {
83
83
  }
84
84
  return contents;
85
85
  };
86
- /**
87
- * A entrada de UMA versão, para ser o corpo do release publicado.
88
- *
89
- * O texto do release não é escrito à mão em lugar nenhum: ele é o mesmo parágrafo que já foi conferido
90
- * pelo `pack`, recortado daqui. Dois textos sobre a mesma versão divergem — e o que divergiria é
91
- * justamente o que fica visível para quem procura o que mudou.
92
- *
93
- * Recorta do cabeçalho da versão até o próximo cabeçalho, e não até o fim: o que vem depois é o
94
- * histórico anterior, e ele já está publicado nos releases dele.
95
- */
96
- export const changelogEntry = (contents, version) => {
86
+ export const changelogEntries = (contents, version, since) => {
97
87
  const body = withoutTemplates(contents);
98
88
  const headings = [...body.matchAll(ENTRY)];
99
- const index = headings.findIndex((match) => match[1] === version);
100
- if (index === -1) {
89
+ const first = headings.findIndex((match) => match[1] === version);
90
+ if (first === -1) {
101
91
  throw new Error(`The changelog has no entry for ${version}.`);
102
92
  }
103
- const start = headings[index].index;
104
- const end = index + 1 < headings.length ? headings[index + 1].index : body.length;
105
- return `${body.slice(start, end).trim()}\n`;
93
+ let last = first;
94
+ while (since !== undefined
95
+ && last + 1 < headings.length
96
+ && compareVersions(headings[last + 1][1], since) > 0) {
97
+ last += 1;
98
+ }
99
+ const start = headings[first].index;
100
+ const end = last + 1 < headings.length ? headings[last + 1].index : body.length;
101
+ return {
102
+ body: `${body.slice(start, end).trim()}\n`,
103
+ versions: headings.slice(first, last + 1).map((match) => match[1]),
104
+ };
106
105
  };
107
106
  /**
108
107
  * Põe os dois históricos na raiz da instalação, SOBREPONDO — pela mesma razão que o `MICROTEIA.md` é
@@ -1,19 +1,33 @@
1
1
  /**
2
- * As extensões de primeira parte, empacotadas como FONTE — `plugin.ts` e as views em `.tsx` —, uma pasta
3
- * por extensão.
2
+ * Arquivo distribuído → módulo de origem, relativo a `src/plugins/marketplace/`. O nome da pasta do
3
+ * exemplo é o do `.json`, sem extensão.
4
4
  *
5
- * Viajavam como `.json`. Um `.json` de 90 KB não ensina nada: é o artefato, e quem abre não vê como se
6
- * chega nele. O que ensina é a fonte, e ela já viaja de qualquer forma — o código das views está dentro
7
- * do `.json`, em string. Passar a mandar o `.ts` não expõe nada novo; expõe de forma legível o que já ia.
5
+ * Há DUAS formas de origem, e a diferença não é de estilo: é de quem o módulo é.
8
6
  *
9
- * A única transformação é de IMPORT: neste repositório um plugin importa `../../domain/entities/...` e
10
- * `../contributionBuilders`; do lado do escritório, tudo isso é `@microteia/sdk`. Todo símbolo usado
11
- * pelos doze está no barril do SDK — e é por isso que a conferência é possível: `exampleSources.test.ts`
12
- * COMPILA cada exemplo reescrito e exige que o `.json` saia byte a byte igual ao versionado. Um símbolo
13
- * que faltasse no barril reprovaria ali, e não no escritório de alguém.
7
+ * - **Um arquivo** (`automation.ts`) — a extensão mora na raiz do marketplace porque ela é, ou já foi,
8
+ * uma extensão de PRIMEIRA PARTE do catálogo embutido. Os vizinhos que ela importa são descobertos
9
+ * por `siblingModules`, e as views dela ficam em `views/<módulo>/`.
10
+ * - **Um diretório** (`examples/report-example`) — a extensão existe SÓ para ser lida. Nada dela entra
11
+ * no `MARKETPLACE_CATALOG`, então ela não tem por que ocupar a raiz do marketplace junto das que
12
+ * entram: ali as cinco de primeira parte se perderiam no meio de doze moldes. Tudo o que é dela mora
13
+ * na pasta dela — o `plugin.ts`, os catálogos de mensagem e as `views/`.
14
14
  */
15
- /** Arquivo distribuído → módulo de origem. O nome da pasta do exemplo é o do `.json`, sem extensão. */
16
15
  export declare const EXAMPLE_SOURCES: Record<string, string>;
16
+ /**
17
+ * Os exemplos que existem SÓ para ser lidos — os que não estão no catálogo embutido.
18
+ *
19
+ * A pergunta que eles respondem é do `pack`: de quem é o backend que este repositório sobe. Uma
20
+ * extensão de primeira parte que declare `manifest.backend` reivindica uma function que o kernel
21
+ * compila e deploya, e o `pack` confere isso — reivindicar um nome que não existe aborta o
22
+ * empacotamento, e reivindicar um que existe o TIRA da baseline. Um exemplo não é nada disso: o
23
+ * `functions/` dele viaja como fonte para ser lido, e quem o instalar de verdade constrói o próprio.
24
+ * Sem esta distinção, o primeiro exemplo a ensinar Cloud Functions reprovaria toda publicação.
25
+ *
26
+ * É DERIVADA da forma da origem, e não uma segunda lista a manter: morar em `examples/` é
27
+ * exatamente o que significa não estar no catálogo embutido. As cinco de primeira parte moram na raiz
28
+ * do marketplace porque o bundle as carrega, e é por isso que elas continuam sendo cobradas.
29
+ */
30
+ export declare const READING_ONLY_EXAMPLES: ReadonlySet<string>;
17
31
  /**
18
32
  * Reescreve os imports de um plugin para a forma que o escritório usa.
19
33
  *
@@ -10,19 +10,71 @@ import { join } from 'node:path';
10
10
  *
11
11
  * A única transformação é de IMPORT: neste repositório um plugin importa `../../domain/entities/...` e
12
12
  * `../contributionBuilders`; do lado do escritório, tudo isso é `@microteia/sdk`. Todo símbolo usado
13
- * pelos doze está no barril do SDK — e é por isso que a conferência é possível: `exampleSources.test.ts`
13
+ * por eles está no barril do SDK — e é por isso que a conferência é possível: `exampleSources.test.ts`
14
14
  * COMPILA cada exemplo reescrito e exige que o `.json` saia byte a byte igual ao versionado. Um símbolo
15
15
  * que faltasse no barril reprovaria ali, e não no escritório de alguém.
16
16
  */
17
- /** Arquivo distribuído → módulo de origem. O nome da pasta do exemplo é o do `.json`, sem extensão. */
17
+ /** A pasta, dentro do marketplace, em que mora um exemplo que existe só para ser lido. */
18
+ const EXAMPLES_DIR = 'examples';
19
+ /**
20
+ * Arquivo distribuído → módulo de origem, relativo a `src/plugins/marketplace/`. O nome da pasta do
21
+ * exemplo é o do `.json`, sem extensão.
22
+ *
23
+ * Há DUAS formas de origem, e a diferença não é de estilo: é de quem o módulo é.
24
+ *
25
+ * - **Um arquivo** (`automation.ts`) — a extensão mora na raiz do marketplace porque ela é, ou já foi,
26
+ * uma extensão de PRIMEIRA PARTE do catálogo embutido. Os vizinhos que ela importa são descobertos
27
+ * por `siblingModules`, e as views dela ficam em `views/<módulo>/`.
28
+ * - **Um diretório** (`examples/report-example`) — a extensão existe SÓ para ser lida. Nada dela entra
29
+ * no `MARKETPLACE_CATALOG`, então ela não tem por que ocupar a raiz do marketplace junto das que
30
+ * entram: ali as cinco de primeira parte se perderiam no meio de doze moldes. Tudo o que é dela mora
31
+ * na pasta dela — o `plugin.ts`, os catálogos de mensagem e as `views/`.
32
+ */
18
33
  export const EXAMPLE_SOURCES = {
19
- 'automation-example': 'automation.ts',
34
+ 'automation-example': 'examples/automation-example',
35
+ agent: 'agent.ts',
20
36
  brasil: 'brasil.ts',
21
37
  calendar: 'calendar.ts',
22
38
  dashboard: 'dashboard.ts',
23
39
  'locale-pt-br': 'localePtBr',
24
40
  news: 'news.ts',
41
+ 'report-example': 'examples/report-example',
42
+ 'widget-example': 'examples/widget-example',
43
+ 'access-example': 'examples/access-example',
44
+ 'backend-example': 'examples/backend-example',
45
+ 'library-example': 'examples/library-example',
46
+ 'consumer-example': 'examples/consumer-example',
47
+ 'patch-example': 'examples/patch-example',
48
+ 'theme-example': 'examples/theme-example',
49
+ 'auth-example': 'examples/auth-example',
50
+ 'command-example': 'examples/command-example',
51
+ 'settings-example': 'examples/settings-example',
25
52
  };
53
+ /**
54
+ * Os exemplos que existem SÓ para ser lidos — os que não estão no catálogo embutido.
55
+ *
56
+ * A pergunta que eles respondem é do `pack`: de quem é o backend que este repositório sobe. Uma
57
+ * extensão de primeira parte que declare `manifest.backend` reivindica uma function que o kernel
58
+ * compila e deploya, e o `pack` confere isso — reivindicar um nome que não existe aborta o
59
+ * empacotamento, e reivindicar um que existe o TIRA da baseline. Um exemplo não é nada disso: o
60
+ * `functions/` dele viaja como fonte para ser lido, e quem o instalar de verdade constrói o próprio.
61
+ * Sem esta distinção, o primeiro exemplo a ensinar Cloud Functions reprovaria toda publicação.
62
+ *
63
+ * É DERIVADA da forma da origem, e não uma segunda lista a manter: morar em `examples/` é
64
+ * exatamente o que significa não estar no catálogo embutido. As cinco de primeira parte moram na raiz
65
+ * do marketplace porque o bundle as carrega, e é por isso que elas continuam sendo cobradas.
66
+ */
67
+ export const READING_ONLY_EXAMPLES = new Set(Object.entries(EXAMPLE_SOURCES)
68
+ .filter(([, origin]) => origin.startsWith(`${EXAMPLES_DIR}/`))
69
+ .map(([id]) => id));
70
+ /**
71
+ * Os módulos do kernel cuja casa, para quem escreve de fora, é `@microteia/sdk/node` — a entrada de
72
+ * FERRAMENTA do pacote, e não o barril isomórfico.
73
+ *
74
+ * `scriptSource` está aqui porque um `Script` viaja com `sourceTs` E `compiledJs`, e o sandbox nunca
75
+ * transpila: quem empacota precisa produzir o segundo, com o MESMO transpilador que o runtime usa.
76
+ */
77
+ const NODE_ONLY_MODULES = ['sdk/node/', 'domain/services/scriptSource'];
26
78
  /**
27
79
  * Reescreve os imports de um plugin para a forma que o escritório usa.
28
80
  *
@@ -31,6 +83,14 @@ export const EXAMPLE_SOURCES = {
31
83
  * extensão tem a sua pasta — aqui elas moram num diretório comum, com um nível a mais.
32
84
  */
33
85
  export const rewriteImports = (source) => source.replace(/from '(\.[^']*)'/g, (match, specifier) => {
86
+ // A FERRAMENTA vem antes da superfície, e a ordem é o ponto: estes caminhos também casariam com as
87
+ // regras genéricas abaixo, que os mandariam para o barril isomórfico — onde `transpileScriptSource`
88
+ // NÃO está, de propósito, porque ele arrasta o `sucrase` e `@microteia/sdk` é a superfície que não
89
+ // arrasta nada. Do lado do escritório ele vem de `@microteia/sdk/node`, que é outra entrada do
90
+ // mesmo pacote. O sintoma seria no escritório de quem instalou, não aqui, se o round-trip byte a
91
+ // byte não compilasse cada exemplo reescrito.
92
+ if (NODE_ONLY_MODULES.some((one) => specifier.includes(one)))
93
+ return "from '@microteia/sdk/node'";
34
94
  if (/(^|\/)contributionBuilders$/.test(specifier)
35
95
  || specifier.includes('domain/')
36
96
  || specifier.includes('sdk/')) {
@@ -81,9 +141,18 @@ const siblingModules = async (marketplace, entry) => {
81
141
  }
82
142
  return collected;
83
143
  };
84
- const viewFiles = async (viewsDir) => {
144
+ /**
145
+ * Os arquivos de uma pasta do exemplo — `views/` ou `functions/` —, e o teste deles COM eles, de
146
+ * propósito. Pasta ausente é lista vazia, nunca erro: nem todo exemplo tem as duas.
147
+ *
148
+ * O `.rules.test.ts` ao lado de um `.rules.ts` é material didático: é onde se vê que a regra de uma
149
+ * view pode ser extraída e exercitada sem montar React nenhum, que é exatamente o que o `AGENTS.md`
150
+ * pede de quem escreve aqui. O `.test.ts` da RAIZ do módulo é outra coisa — aquele é nosso, sobre a
151
+ * nossa extensão — e é por isso que só ele é filtrado, no laço de baixo.
152
+ */
153
+ const sourceFilesIn = async (folder) => {
85
154
  try {
86
- return (await readdir(viewsDir)).filter((name) => name.endsWith('.tsx') || name.endsWith('.ts'));
155
+ return (await readdir(folder)).filter((name) => name.endsWith('.tsx') || name.endsWith('.ts'));
87
156
  }
88
157
  catch {
89
158
  return [];
@@ -107,7 +176,7 @@ export const stageExamples = async (repo, target) => {
107
176
  }
108
177
  const plugin = origin.replace(/\.ts$/, '');
109
178
  const views = join(marketplace, 'views', plugin);
110
- const files = await viewFiles(views);
179
+ const files = await sourceFilesIn(views);
111
180
  if (files.length > 0)
112
181
  await mkdir(join(directory, 'views'), { recursive: true });
113
182
  for (const name of files) {
@@ -115,14 +184,34 @@ export const stageExamples = async (repo, target) => {
115
184
  }
116
185
  }
117
186
  else {
118
- // Extensão que é um DIRETÓRIO (o pacote de idioma, com um arquivo por namespace). O `index.ts`
119
- // vira `plugin.ts`; os demais são vizinhos dele e os imports relativos seguem valendo.
120
- for (const name of await readdir(join(marketplace, origin))) {
187
+ // Extensão que é um DIRETÓRIO: o exemplo de leitura, cuja entrada é `plugin.ts`, e o pacote de
188
+ // idioma, cuja entrada é `index.ts` porque ele é um módulo de primeira parte antes de ser um
189
+ // exemplo. A entrada vira `plugin.ts` no destino nos dois casos — é o nome que o autor compila —
190
+ // e os demais `.ts` são vizinhos dela, com os imports relativos seguindo valendo.
191
+ const source = join(marketplace, origin);
192
+ const names = await readdir(source);
193
+ const entry = names.includes('plugin.ts') ? 'plugin.ts' : 'index.ts';
194
+ for (const name of names) {
121
195
  if (!name.endsWith('.ts') || name.endsWith('.test.ts'))
122
196
  continue;
123
- const source = rewriteImports(await readFile(join(marketplace, origin, name), 'utf8'));
124
- const isEntry = name === 'index.ts';
125
- await writeFile(join(directory, isEntry ? 'plugin.ts' : name), isEntry ? HEADER(id) + source : source);
197
+ const text = rewriteImports(await readFile(join(source, name), 'utf8'));
198
+ const isEntry = name === entry;
199
+ await writeFile(join(directory, isEntry ? 'plugin.ts' : name), isEntry ? HEADER(id) + text : text);
200
+ }
201
+ // As views, as Cloud Functions e os scripts de um exemplo moram DENTRO dele, e não em
202
+ // `views/<módulo>/`: a pasta é o pacote, exatamente como do lado do escritório. `functions/` e
203
+ // `scripts/` viajam VERBATIM — a primeira já é escrita contra `microteia-functions/office` e
204
+ // `firebase-functions`, que são os nomes que valem lá, e a segunda não importa nada: o script
205
+ // roda no sandbox, contra globais que só existem lá dentro.
206
+ for (const folder of ['views', 'functions', 'scripts']) {
207
+ const from = join(source, folder);
208
+ const files = await sourceFilesIn(from);
209
+ if (files.length === 0)
210
+ continue;
211
+ await mkdir(join(directory, folder), { recursive: true });
212
+ for (const name of files) {
213
+ await writeFile(join(directory, folder, name), await readFile(join(from, name), 'utf8'));
214
+ }
126
215
  }
127
216
  }
128
217
  written.push(id);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Os `extension-templates/*.json` cujo backend é DESTE repositório — a única porta para essa pergunta.
3
+ *
4
+ * `extension-templates/` guarda duas coisas com a mesma forma e donos diferentes: as extensões de
5
+ * primeira parte, que viajam no bundle e cujas Cloud Functions o kernel compila e deploya, e os
6
+ * exemplos, que viajam como fonte para serem lidos e cujo backend pertence a quem os instalar. A conta
7
+ * do `pack` — baseline = todas as functions menos as reivindicadas — só faz sentido sobre as primeiras.
8
+ *
9
+ * A porta é única porque a pergunta já estava em dois lugares: o `pack` e o teste que guarda o `pack`.
10
+ * Dois leitores do mesmo diretório com filtros diferentes é como um portão passa a aprovar o que ele
11
+ * existe para reprovar — aqui, o exemplo que ensina Cloud Functions abortando toda publicação, ou uma
12
+ * function saindo da baseline do kernel porque um MOLDE a reivindicou.
13
+ */
14
+ export declare const firstPartyTemplates: <T>(root: string) => T[];
@@ -0,0 +1,23 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { READING_ONLY_EXAMPLES } from './exampleSources.js';
4
+ /**
5
+ * Os `extension-templates/*.json` cujo backend é DESTE repositório — a única porta para essa pergunta.
6
+ *
7
+ * `extension-templates/` guarda duas coisas com a mesma forma e donos diferentes: as extensões de
8
+ * primeira parte, que viajam no bundle e cujas Cloud Functions o kernel compila e deploya, e os
9
+ * exemplos, que viajam como fonte para serem lidos e cujo backend pertence a quem os instalar. A conta
10
+ * do `pack` — baseline = todas as functions menos as reivindicadas — só faz sentido sobre as primeiras.
11
+ *
12
+ * A porta é única porque a pergunta já estava em dois lugares: o `pack` e o teste que guarda o `pack`.
13
+ * Dois leitores do mesmo diretório com filtros diferentes é como um portão passa a aprovar o que ele
14
+ * existe para reprovar — aqui, o exemplo que ensina Cloud Functions abortando toda publicação, ou uma
15
+ * function saindo da baseline do kernel porque um MOLDE a reivindicou.
16
+ */
17
+ export const firstPartyTemplates = (root) => {
18
+ const dir = join(root, 'extension-templates');
19
+ return readdirSync(dir)
20
+ .filter((file) => file.endsWith('.json'))
21
+ .filter((file) => !READING_ONLY_EXAMPLES.has(basename(file, '.json')))
22
+ .map((file) => JSON.parse(readFileSync(join(dir, file), 'utf8')));
23
+ };
@@ -254,7 +254,7 @@ ${examples.length === 0 ? '_None in this package._' : examples.map((id) => `- \`
254
254
  Each is the real source of an extension that ships with Microteia. Build any of them and you get the
255
255
  same \`.json\` the platform installs:
256
256
 
257
- npx microteia-plugin build examples/pessoa/plugin.ts
257
+ npx microteia-plugin build examples/${examples[0] ?? '<example>'}/plugin.ts
258
258
 
259
259
  ## 6. An AI assistant in this directory
260
260
 
@@ -13,6 +13,7 @@ import { KERNEL_CHANGELOG, readChangelog } from './changelogs.js';
13
13
  import { assertInsideRepository } from './repository.js';
14
14
  import { SDK_TARBALL } from './pluginWorkspaceTemplates.js';
15
15
  import { stageExamples } from './exampleSources.js';
16
+ import { firstPartyTemplates } from './firstPartyTemplates.js';
16
17
  import { packageCustomIndexTs, packageCustomTsconfig, packageFirebaseJson, packageFunctionsIndexJs, packageFunctionsPackageJson, } from './packageTemplates.js';
17
18
  /**
18
19
  * `dist` é um modo do Vite para o qual NÃO existe `.env.dist`. É isso que garante um bundle sem
@@ -23,13 +24,13 @@ const DISTRIBUTION_TARGET = 'dist';
23
24
  * As exigências de backend das extensões de PRIMEIRA PARTE saem dos `extension-templates/*.json`, que
24
25
  * são gerados dos `.ts` e conferidos pelo `verify-extensions` que o gate de qualidade acima já roda.
25
26
  * Ler o `.json` gerado em vez do `.ts` evita carregar o Vite aqui só para avaliar um módulo.
27
+ *
28
+ * Os exemplos que só viajam para ser lidos ficam de FORA (`READING_ONLY_EXAMPLES`), e o motivo está
29
+ * escrito lá: o backend de um exemplo não é deste repositório. Cobrá-lo aqui faria o exemplo que
30
+ * ensina Cloud Functions abortar toda publicação — e, se a function existisse, a reivindicação a
31
+ * tiraria da baseline do kernel, que é o oposto exato do que um exemplo deve provocar.
26
32
  */
27
- const firstPartyClaims = async (root) => {
28
- const dir = join(root, 'extension-templates');
29
- const files = (await readdir(dir)).filter((file) => file.endsWith('.json'));
30
- const templates = await Promise.all(files.map((file) => readJson(join(dir, file))));
31
- return claimsFromTemplates(templates);
32
- };
33
+ const firstPartyClaims = (root) => claimsFromTemplates(firstPartyTemplates(root));
33
34
  const readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
34
35
  /**
35
36
  * Os `defineString` e as callables estão espalhados pelos handlers, não num arquivo só — então a
@@ -131,7 +132,7 @@ export const pack = async (args) => {
131
132
  // ausência que é só do parser.
132
133
  const callables = scan.callables.filter((name) => functionNames.includes(name));
133
134
  const rootIndexes = await readJson(join(root, 'firestore.indexes.json'));
134
- const claims = await firstPartyClaims(root);
135
+ const claims = firstPartyClaims(root);
135
136
  const problems = claimProblems(claims, functionNames, rootIndexes.indexes);
136
137
  if (problems.length > 0) {
137
138
  throw new Error('An extension declares backend artifacts that do not add up, so the package would be wrong:\n'
@@ -6,8 +6,8 @@ import { CLI_VERSION, repoRoot } from '../shared/paths.js';
6
6
  import { capture, captureOrThrow, run } from '../shared/run.js';
7
7
  import { blank, bullets, heading, info, pending, step, success, warn } from '../shared/ui.js';
8
8
  import { digestMismatch, digestOfFile } from '../shared/digest.js';
9
- import { emptyIndex, entryOf, kernelTarballName, parseIndex, serializeIndex, withEntry, } from '../kernel/channel.js';
10
- import { CONSOLE_CHANGELOG, KERNEL_CHANGELOG, changelogEntry, readChangelog } from './changelogs.js';
9
+ import { emptyIndex, entryOf, kernelTarballName, parseIndex, publishedBefore, serializeIndex, withEntry, } from '../kernel/channel.js';
10
+ import { CONSOLE_CHANGELOG, KERNEL_CHANGELOG, changelogEntries, readChangelog } from './changelogs.js';
11
11
  import { CONSOLE_PACKAGE_NAME } from './packageTemplates.js';
12
12
  import { assertInsideRepository } from './repository.js';
13
13
  /**
@@ -170,20 +170,30 @@ const readKernelPackage = async (root, version) => {
170
170
  + 'One of the two was rebuilt without the other. Run "npm run pack:kernel" again.');
171
171
  }
172
172
  const changelog = await readChangelog(root, KERNEL_CHANGELOG, version);
173
- return { manifest, tarball, entry: changelogEntry(changelog, version) };
173
+ return { manifest, tarball, changelog };
174
174
  };
175
175
  const publishKernel = async (root, version, repo, dryRun) => {
176
176
  await assertGitHubCli();
177
- const { manifest, tarball, entry } = await readKernelPackage(root, version);
177
+ const { manifest, tarball, changelog } = await readKernelPackage(root, version);
178
178
  const directory = join(root, 'distributions', 'kernel', version);
179
179
  const manifestPath = join(directory, 'manifest.json');
180
180
  const published = await readPublishedIndex(repo);
181
+ /**
182
+ * O corpo do release cobre o INTERVALO, e não só a versão que está saindo. Um pacote leva tudo o que
183
+ * veio antes dele; um release que só cita a própria entrada esconde as versões que foram empacotadas e
184
+ * não publicadas, e nenhum release posterior volta para contá-las.
185
+ */
186
+ const since = publishedBefore(published.index, version);
187
+ const notes = changelogEntries(changelog, version, since);
181
188
  const next = withEntry(published.index, entryOf(manifest, {
182
189
  tarball: assetUrl(repo, version, kernelTarballName(version)),
183
190
  manifest: assetUrl(repo, version, 'manifest.json'),
184
191
  }, new Date().toISOString()));
185
192
  success(`kernel ${version} verified: ${manifest.tarball?.bytes} bytes, sha256 ${manifest.tarball?.sha256.slice(0, 12)}…`);
186
193
  info(` tag ${tagOf(version)} in ${repo}, and ${INDEX_PATH} promoting it to "latest"`);
194
+ info(notes.versions.length === 1
195
+ ? ` release notes: the ${version} entry of ${KERNEL_CHANGELOG}`
196
+ : ` release notes: ${notes.versions.join(', ')} — every entry since ${since ?? 'the first one'}`);
187
197
  if (dryRun) {
188
198
  if (published.empty)
189
199
  info(` ${repo} has no commits, so it would be seeded with a README first`);
@@ -192,9 +202,9 @@ const publishKernel = async (root, version, repo, dryRun) => {
192
202
  }
193
203
  if (published.empty)
194
204
  await seedRepository(repo);
195
- const notes = await mkdtemp(join(tmpdir(), 'microteia-publish-'));
196
- const notesFile = join(notes, 'notes.md');
197
- await writeFile(notesFile, entry);
205
+ const notesRoot = await mkdtemp(join(tmpdir(), 'microteia-publish-'));
206
+ const notesFile = join(notesRoot, 'notes.md');
207
+ await writeFile(notesFile, notes.body);
198
208
  try {
199
209
  if (await releaseExists(repo, version)) {
200
210
  /**
@@ -224,7 +234,7 @@ const publishKernel = async (root, version, repo, dryRun) => {
224
234
  success(`${INDEX_PATH} updated — "microteia unpack --version latest" now takes ${version}`);
225
235
  }
226
236
  finally {
227
- await rm(notes, { recursive: true, force: true });
237
+ await rm(notesRoot, { recursive: true, force: true });
228
238
  }
229
239
  };
230
240
  const publishedConsoleVersion = async (version) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microteia/console",
3
- "version": "2.20.0",
3
+ "version": "2.22.1",
4
4
  "description": "Install and update Microteia in a Firebase project you own.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",