@microteia/console 2.7.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/CONSOLE-VERSIONS.md +85 -0
- package/README.md +64 -0
- package/lib/console/backendEnv.d.ts +52 -0
- package/lib/console/backendEnv.js +118 -0
- package/lib/console/backendIndexes.d.ts +40 -0
- package/lib/console/backendIndexes.js +178 -0
- package/lib/console/backendParameters.d.ts +48 -0
- package/lib/console/backendParameters.js +100 -0
- package/lib/console/callableAccess.d.ts +39 -0
- package/lib/console/callableAccess.js +61 -0
- package/lib/console/channel.d.ts +49 -0
- package/lib/console/channel.js +145 -0
- package/lib/console/connect.d.ts +2 -0
- package/lib/console/connect.js +74 -0
- package/lib/console/customFunctions.d.ts +120 -0
- package/lib/console/customFunctions.js +298 -0
- package/lib/console/deploy.d.ts +2 -0
- package/lib/console/deploy.js +261 -0
- package/lib/console/deployFunctions.d.ts +45 -0
- package/lib/console/deployFunctions.js +204 -0
- package/lib/console/environment.d.ts +62 -0
- package/lib/console/environment.js +151 -0
- package/lib/console/firebaseCli.d.ts +15 -0
- package/lib/console/firebaseCli.js +90 -0
- package/lib/console/firestoreRest.d.ts +39 -0
- package/lib/console/firestoreRest.js +131 -0
- package/lib/console/hostingCheck.d.ts +29 -0
- package/lib/console/hostingCheck.js +44 -0
- package/lib/console/init.d.ts +2 -0
- package/lib/console/init.js +60 -0
- package/lib/console/pluginWorkspace.d.ts +48 -0
- package/lib/console/pluginWorkspace.js +148 -0
- package/lib/console/provision.d.ts +70 -0
- package/lib/console/provision.js +478 -0
- package/lib/console/regionOrphans.d.ts +43 -0
- package/lib/console/regionOrphans.js +99 -0
- package/lib/console/runtimeConfigShape.d.ts +25 -0
- package/lib/console/runtimeConfigShape.js +35 -0
- package/lib/console/unpack.d.ts +9 -0
- package/lib/console/unpack.js +333 -0
- package/lib/console/versions.d.ts +10 -0
- package/lib/console/versions.js +72 -0
- package/lib/console/workspace.d.ts +19 -0
- package/lib/console/workspace.js +33 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +136 -0
- package/lib/kernel/backendShape.d.ts +74 -0
- package/lib/kernel/backendShape.js +91 -0
- package/lib/kernel/channel.d.ts +79 -0
- package/lib/kernel/channel.js +157 -0
- package/lib/kernel/manifest.d.ts +138 -0
- package/lib/kernel/manifest.js +145 -0
- package/lib/shared/args.d.ts +14 -0
- package/lib/shared/args.js +44 -0
- package/lib/shared/digest.d.ts +30 -0
- package/lib/shared/digest.js +34 -0
- package/lib/shared/paths.d.ts +90 -0
- package/lib/shared/paths.js +97 -0
- package/lib/shared/run.d.ts +28 -0
- package/lib/shared/run.js +49 -0
- package/lib/shared/ui.d.ts +23 -0
- package/lib/shared/ui.js +43 -0
- package/lib/vendor/authoringGuide.d.ts +19 -0
- package/lib/vendor/authoringGuide.js +284 -0
- package/lib/vendor/bundleAudit.d.ts +19 -0
- package/lib/vendor/bundleAudit.js +95 -0
- package/lib/vendor/changelogs.d.ts +62 -0
- package/lib/vendor/changelogs.js +131 -0
- package/lib/vendor/exampleSources.d.ts +29 -0
- package/lib/vendor/exampleSources.js +126 -0
- package/lib/vendor/installationDocs.d.ts +35 -0
- package/lib/vendor/installationDocs.js +243 -0
- package/lib/vendor/pack.d.ts +2 -0
- package/lib/vendor/pack.js +276 -0
- package/lib/vendor/packConsole.d.ts +13 -0
- package/lib/vendor/packConsole.js +63 -0
- package/lib/vendor/packageTemplates.d.ts +66 -0
- package/lib/vendor/packageTemplates.js +200 -0
- package/lib/vendor/pluginWorkspaceTemplates.d.ts +19 -0
- package/lib/vendor/pluginWorkspaceTemplates.js +29 -0
- package/lib/vendor/publish.d.ts +2 -0
- package/lib/vendor/publish.js +277 -0
- package/lib/vendor/repository.d.ts +9 -0
- package/lib/vendor/repository.js +26 -0
- package/package.json +21 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Microteia Console — changelog
|
|
2
|
+
|
|
3
|
+
> Technical history of the Console — the command that installs, upgrades and publishes Microteia in a
|
|
4
|
+
> Firebase project. Newest first, one entry per `CLI_VERSION`.
|
|
5
|
+
>
|
|
6
|
+
> The Console is versioned and distributed **separately** from the platform: `CLI_VERSION` does not
|
|
7
|
+
> follow the package's `version`, and a package states the oldest Console it can be installed by
|
|
8
|
+
> (`minimumCliVersion`). So the two histories are two files. The platform's is `KERNEL-VERSIONS.md`,
|
|
9
|
+
> beside this one.
|
|
10
|
+
>
|
|
11
|
+
> **This file travels with the Console**, and `microteia unpack` replaces the copy next to your
|
|
12
|
+
> `microteia.yaml` on every run. Do not edit that copy.
|
|
13
|
+
>
|
|
14
|
+
> Semver on the TOOL's side: major when a command changes contract, minor when it gains a capability,
|
|
15
|
+
> patch when what it writes changes without the package coming to depend on it.
|
|
16
|
+
|
|
17
|
+
<!-- A new entry goes here, above the previous ones, in this format:
|
|
18
|
+
|
|
19
|
+
#### [9.9.9] — YYYY-MM-DD
|
|
20
|
+
|
|
21
|
+
- feat(command): what the command now does.
|
|
22
|
+
- fix(command): what was broken, and what it did to whoever hit it.
|
|
23
|
+
-->
|
|
24
|
+
|
|
25
|
+
#### [2.7.0] — 2026-08-24
|
|
26
|
+
|
|
27
|
+
- feat(unpack): `--version latest` and `--version <x.y.z>` take a platform version from the published
|
|
28
|
+
catalog — read it, download it, verify its size and sha256, and cache it under `~/.microteia/cache`,
|
|
29
|
+
outside the installation because `.microteia/` is deleted whole on every kernel swap. `--kernel
|
|
30
|
+
<package>.tgz` is unchanged and is not legacy: while installing depends on a download, a machine with
|
|
31
|
+
no access to the catalog cannot install anything, and the installation already depends on enough that
|
|
32
|
+
is not ours. A cache entry whose digest does not match is deleted and fetched again, so there is no
|
|
33
|
+
flag to bypass it — the person who would need one is the one who would not think to pass it.
|
|
34
|
+
- fix(unpack): the refusal by `minimumCliVersion` happens BEFORE the previous install is touched.
|
|
35
|
+
Extracting and swapping were one step, and the swap came before the manifest was read: a package
|
|
36
|
+
demanding a newer Console deleted the `install/` of the version that worked and only then refused,
|
|
37
|
+
leaving the installation with no kernel at all — because of a check that exists precisely so that
|
|
38
|
+
nothing would happen. It is now extract, verify, then commit, and a refusal removes the staging it
|
|
39
|
+
opened.
|
|
40
|
+
- feat(publish): releases what `pack` and `pack-console` built — assets to a releases-only repository
|
|
41
|
+
through `gh`, the Console to npm, and the catalog last. That order is the decision: a catalog pointing
|
|
42
|
+
at a file that did not upload breaks every installation asking for `latest`, while a file the catalog
|
|
43
|
+
does not yet mention breaks nothing. An interrupted publish is resumed rather than refused, since
|
|
44
|
+
refusing would leave deleting the release by hand as the only way out. It does not build — `pack` is
|
|
45
|
+
what runs the quality gate — it re-derives the tarball's sha256 instead of trusting the manifest
|
|
46
|
+
beside it, and it refuses a version already in the catalog: a released version is never rewritten.
|
|
47
|
+
- feat(pack-console): the packed folder is publishable as `@microteia/console`, so an office installs
|
|
48
|
+
the Console with `npm i -g @microteia/console` or runs it with `npx`. This closes a chicken-and-egg:
|
|
49
|
+
while the Console itself had to be downloaded from somewhere, someone had to download the downloader,
|
|
50
|
+
and the first step of installing was a folder received by e-mail and an `ln -s` written by hand. The
|
|
51
|
+
registry already provides the versioned URL, the lockfile fingerprint, the CDN and `npm outdated` that
|
|
52
|
+
a channel would have had to reimplement. `CONSOLE-VERSIONS.md` is in `files` for a reason that is not
|
|
53
|
+
documentation: `unpack` reads it beside the running binary, and outside `files` it does not exist in a
|
|
54
|
+
global install.
|
|
55
|
+
- feat(--version): says when a newer kernel is published, and the lookup is injected rather than called
|
|
56
|
+
from the report. `--version` is the first command run when something is odd and the answer people paste
|
|
57
|
+
into a request for help; it answers about the installation, so on a machine with no network it answers
|
|
58
|
+
the same.
|
|
59
|
+
- feat(publish): `--otp <code>` reaches `npm publish`, and a refused publish prints what the registry
|
|
60
|
+
requires. The registry accepts neither a plain `npm login` token nor an account without 2FA, so
|
|
61
|
+
without a way through the command it was unusable: it called `npm publish`, got a 403, and the only
|
|
62
|
+
way out was running `npm` by hand — which skips the changelog check this command exists to make. The
|
|
63
|
+
note is printed on ANY failure and does not claim the cause: `npm`'s output goes straight to the
|
|
64
|
+
terminal, so what reaches the catch is "Command failed (exit 1)" and nothing else. A first version
|
|
65
|
+
looked for "2fa" in that text and never once printed.
|
|
66
|
+
- fix(connect): the last line printed `microteia deploy --kernel <pkg>.tgz`. That flag left `deploy` in
|
|
67
|
+
`2.0.0`, so the line had been sending whoever just connected straight into a refusal — on the command
|
|
68
|
+
right after the one that worked. It now prints `unpack --version latest` and `deploy`.
|
|
69
|
+
|
|
70
|
+
#### [2.6.0] — 2026-08-23
|
|
71
|
+
|
|
72
|
+
- feat(unpack): writes `KERNEL-VERSIONS.md` and `CONSOLE-VERSIONS.md` into the installation root, and
|
|
73
|
+
replaces both on every run. The first is copied out of the unpacked package, the second comes from
|
|
74
|
+
the Console binary that is running — each history written by the artifact it belongs to, which is
|
|
75
|
+
the only way neither can describe a version that is not there.
|
|
76
|
+
- feat(pack-console): the packed folder carries `CONSOLE-VERSIONS.md`, and packaging refuses a
|
|
77
|
+
`CLI_VERSION` this file has no entry for.
|
|
78
|
+
- docs(unpack): the generated `MICROTEIA.md` and the seeded `README.md` list both changelogs in the
|
|
79
|
+
directory map, so the files are found without being looked for.
|
|
80
|
+
- docs(readme): the packed README no longer tells you to run `microteia deploy --kernel <pkg>.tgz`.
|
|
81
|
+
That flag was removed in `2.0.0` — the package arrives through `unpack` — and the line had been
|
|
82
|
+
sending whoever installed to a refusal ever since.
|
|
83
|
+
|
|
84
|
+
Entries before `2.6.0` are in the `CLI_VERSION` block of `cli/src/shared/paths.ts`, in the language of
|
|
85
|
+
the team that wrote them. They stay there; this file is where the history continues.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Microteia Console 2.7.0
|
|
2
|
+
|
|
3
|
+
Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
|
|
4
|
+
what it needs are command-line tools that are already on the machine, listed at the bottom.
|
|
5
|
+
|
|
6
|
+
## Installing it
|
|
7
|
+
|
|
8
|
+
npm i -g @microteia/console
|
|
9
|
+
microteia --help
|
|
10
|
+
|
|
11
|
+
Or run it without installing anything:
|
|
12
|
+
|
|
13
|
+
npx @microteia/console --help
|
|
14
|
+
|
|
15
|
+
Where npm's global prefix belongs to root — Node installed system-wide under `/usr/local` — the global
|
|
16
|
+
install fails with EACCES. Point npm at a prefix of your own instead of reaching for sudo, which would
|
|
17
|
+
write root-owned files that then need sudo to remove:
|
|
18
|
+
|
|
19
|
+
npm config set prefix ~/.npm-global
|
|
20
|
+
export PATH="$HOME/.npm-global/bin:$PATH" # add to ~/.zshrc
|
|
21
|
+
|
|
22
|
+
This folder also runs as it is, with no npm at all:
|
|
23
|
+
|
|
24
|
+
node lib/index.js --help
|
|
25
|
+
|
|
26
|
+
## Using it
|
|
27
|
+
|
|
28
|
+
microteia init --name "<office>"
|
|
29
|
+
microteia connect --project <your-firebase-project-id>
|
|
30
|
+
microteia unpack --version latest
|
|
31
|
+
microteia deploy
|
|
32
|
+
|
|
33
|
+
`connect` prints everything that has to be prepared in the Firebase project first — start with the
|
|
34
|
+
Blaze plan, since nothing else can be created without it.
|
|
35
|
+
|
|
36
|
+
`unpack --version` reads the published catalog, downloads the version and checks its sha256 before
|
|
37
|
+
touching what is installed. On a machine that cannot reach the catalog, name a file you already have:
|
|
38
|
+
|
|
39
|
+
microteia unpack --kernel <microteia-kernel-x.y.z>.tgz
|
|
40
|
+
|
|
41
|
+
`microteia --version` answers what is installed here, and says so when a newer kernel is published.
|
|
42
|
+
|
|
43
|
+
Later, whenever an office installs an extension that needs server-side resources, run:
|
|
44
|
+
|
|
45
|
+
microteia provision
|
|
46
|
+
|
|
47
|
+
The extension shows "Backend pending" in the application until you do, and stays out of the menu.
|
|
48
|
+
|
|
49
|
+
## What has to exist on this machine
|
|
50
|
+
|
|
51
|
+
| Tool | Used for |
|
|
52
|
+
| ------------- | -------------------------------------------------------------- |
|
|
53
|
+
| Node.js 22+ | running this Console (newer is fine; the functions' own runtime is pinned to 22) |
|
|
54
|
+
| `firebase` | every deploy — authenticate with `firebase login` first |
|
|
55
|
+
| `npm`, `tar` | installing the functions' dependencies and unpacking the kernel |
|
|
56
|
+
| `gcloud` | creating the App Engine application, and reading which extensions an office installed (`gcloud auth login`) |
|
|
57
|
+
|
|
58
|
+
The `pack` and `publish` commands in here build and release the platform itself, and only work inside
|
|
59
|
+
the Microteia repository.
|
|
60
|
+
|
|
61
|
+
## What changed in this Console
|
|
62
|
+
|
|
63
|
+
`CONSOLE-VERSIONS.md`, in this folder. `unpack` copies it into the installation it manages, beside the
|
|
64
|
+
platform's own `KERNEL-VERSIONS.md` — each history travels with the artifact it describes.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { BackendParameter } from '../kernel/manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* As configurações de backend são do ESCRITÓRIO, e por isso moram ao lado do `microteia.yaml` — não
|
|
4
|
+
* dentro de `.microteia/`.
|
|
5
|
+
*
|
|
6
|
+
* Elas moravam lá dentro, e o resultado era perda de dado: o `deploy` substitui o pacote por inteiro
|
|
7
|
+
* (`rm -rf install` e renomeia o staging), preservando explicitamente só `functions/custom` e
|
|
8
|
+
* `functions/node_modules`. Uma credencial preenchida à mão em
|
|
9
|
+
* `.microteia/install/functions/.env.<projectId>` era apagada no deploy seguinte, e o
|
|
10
|
+
* `ensureBackendEnv` a recriava VAZIA — com o agravante de que a impressão digital deixava de bater, o
|
|
11
|
+
* provisionamento marcava as functions como obsoletas e as redeployava com a string vazia. Ou seja: o
|
|
12
|
+
* mecanismo escrito para detectar valor faltando passava a ser a causa dele.
|
|
13
|
+
*
|
|
14
|
+
* O diretório do `microteia.yaml` é o repositório de quem mantém a instalação: é onde ficam as
|
|
15
|
+
* extensões que ele escreve (`extensions/`) e o registro do ambiente. `.microteia/` é descartável por
|
|
16
|
+
* definição — tudo ali se refaz do tarball mais um `connect` —, e nada que uma pessoa digita pode
|
|
17
|
+
* depender de sobreviver a isso.
|
|
18
|
+
*
|
|
19
|
+
* O arquivo que o `firebase-functions` lê continua tendo de estar em `install/functions/`, então ele é
|
|
20
|
+
* uma CÓPIA, escrita no deploy. A fonte é a da raiz.
|
|
21
|
+
*/
|
|
22
|
+
/** O arquivo que o escritório edita. O nome leva o projeto: uma instalação, um projeto Firebase. */
|
|
23
|
+
export declare const officeEnvName: (projectId: string) => string;
|
|
24
|
+
export declare const officeEnvPath: (root: string, projectId: string) => string;
|
|
25
|
+
export interface EnsureEnvResult {
|
|
26
|
+
path: string;
|
|
27
|
+
created: boolean;
|
|
28
|
+
added: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Garante que o arquivo existe e declara todo parâmetro do pacote.
|
|
32
|
+
*
|
|
33
|
+
* ACRESCENTA o que falta, nunca reescreve: um parâmetro novo numa versão nova do kernel não pode
|
|
34
|
+
* apagar o que já foi preenchido. E o `firebase-functions` valida os `defineString` GLOBALMENTE, não
|
|
35
|
+
* por function — um parâmetro declarado sem entrada no arquivo recusa o deploy de todas elas, e é por
|
|
36
|
+
* isso que a lista é escrita a partir do manifesto em vez de copiada de um template.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ensureOfficeEnv: (root: string, projectId: string, parameters: BackendParameter[]) => Promise<EnsureEnvResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Copia a fonte para onde o `firebase-functions` a lê. Sem isso o valor preenchido na raiz não chega a
|
|
41
|
+
* function nenhuma, e a falha aparece só quando a feature é usada.
|
|
42
|
+
*/
|
|
43
|
+
export declare const stageOfficeEnv: (root: string, projectId: string, install: string) => Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Garante que o `.env.<projectId>` não vai para um repositório. O diretório é do escritório e tende a
|
|
46
|
+
* ser versionado — é onde ele guarda as extensões que escreve —, e uma credencial commitada por
|
|
47
|
+
* omissão nossa é o pior desfecho possível deste comando.
|
|
48
|
+
*
|
|
49
|
+
* Cria o arquivo quando não existe. Quando existe, ACRESCENTA só a linha que falta e diz que o fez:
|
|
50
|
+
* reescrever o `.gitignore` de alguém seria pior que o problema.
|
|
51
|
+
*/
|
|
52
|
+
export declare const ensureRootGitignore: (root: string) => Promise<"created" | "appended" | "ok">;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { access, appendFile, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* As configurações de backend são do ESCRITÓRIO, e por isso moram ao lado do `microteia.yaml` — não
|
|
5
|
+
* dentro de `.microteia/`.
|
|
6
|
+
*
|
|
7
|
+
* Elas moravam lá dentro, e o resultado era perda de dado: o `deploy` substitui o pacote por inteiro
|
|
8
|
+
* (`rm -rf install` e renomeia o staging), preservando explicitamente só `functions/custom` e
|
|
9
|
+
* `functions/node_modules`. Uma credencial preenchida à mão em
|
|
10
|
+
* `.microteia/install/functions/.env.<projectId>` era apagada no deploy seguinte, e o
|
|
11
|
+
* `ensureBackendEnv` a recriava VAZIA — com o agravante de que a impressão digital deixava de bater, o
|
|
12
|
+
* provisionamento marcava as functions como obsoletas e as redeployava com a string vazia. Ou seja: o
|
|
13
|
+
* mecanismo escrito para detectar valor faltando passava a ser a causa dele.
|
|
14
|
+
*
|
|
15
|
+
* O diretório do `microteia.yaml` é o repositório de quem mantém a instalação: é onde ficam as
|
|
16
|
+
* extensões que ele escreve (`extensions/`) e o registro do ambiente. `.microteia/` é descartável por
|
|
17
|
+
* definição — tudo ali se refaz do tarball mais um `connect` —, e nada que uma pessoa digita pode
|
|
18
|
+
* depender de sobreviver a isso.
|
|
19
|
+
*
|
|
20
|
+
* O arquivo que o `firebase-functions` lê continua tendo de estar em `install/functions/`, então ele é
|
|
21
|
+
* uma CÓPIA, escrita no deploy. A fonte é a da raiz.
|
|
22
|
+
*/
|
|
23
|
+
/** O arquivo que o escritório edita. O nome leva o projeto: uma instalação, um projeto Firebase. */
|
|
24
|
+
export const officeEnvName = (projectId) => `.env.${projectId}`;
|
|
25
|
+
export const officeEnvPath = (root, projectId) => join(root, officeEnvName(projectId));
|
|
26
|
+
const exists = async (path) => {
|
|
27
|
+
try {
|
|
28
|
+
await access(path);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const declaredNames = (content) => new Set(content.split('\n')
|
|
36
|
+
.map((line) => line.trim())
|
|
37
|
+
.filter((line) => line !== '' && !line.startsWith('#') && line.includes('='))
|
|
38
|
+
.map((line) => line.slice(0, line.indexOf('=')).trim()));
|
|
39
|
+
/**
|
|
40
|
+
* Garante que o arquivo existe e declara todo parâmetro do pacote.
|
|
41
|
+
*
|
|
42
|
+
* ACRESCENTA o que falta, nunca reescreve: um parâmetro novo numa versão nova do kernel não pode
|
|
43
|
+
* apagar o que já foi preenchido. E o `firebase-functions` valida os `defineString` GLOBALMENTE, não
|
|
44
|
+
* por function — um parâmetro declarado sem entrada no arquivo recusa o deploy de todas elas, e é por
|
|
45
|
+
* isso que a lista é escrita a partir do manifesto em vez de copiada de um template.
|
|
46
|
+
*/
|
|
47
|
+
export const ensureOfficeEnv = async (root, projectId, parameters) => {
|
|
48
|
+
const path = officeEnvPath(root, projectId);
|
|
49
|
+
const previous = await exists(path) ? await readFile(path, 'utf8') : null;
|
|
50
|
+
const declared = declaredNames(previous ?? '');
|
|
51
|
+
const missing = parameters.filter((parameter) => !declared.has(parameter.name));
|
|
52
|
+
if (previous !== null && missing.length === 0)
|
|
53
|
+
return { path, created: false, added: [] };
|
|
54
|
+
const body = `${missing.map((parameter) => `${parameter.name}=`).join('\n')}\n`;
|
|
55
|
+
if (previous === null) {
|
|
56
|
+
const header = `# Backend settings for ${projectId}.\n`
|
|
57
|
+
+ '#\n'
|
|
58
|
+
+ '# Fill in what the features you use need; the rest stays empty. After changing a value, run\n'
|
|
59
|
+
+ '# "microteia provision" — the value is stamped into each Cloud Function at ITS deploy, so a\n'
|
|
60
|
+
+ '# function already running keeps the previous one until it is deployed again.\n'
|
|
61
|
+
+ '#\n'
|
|
62
|
+
+ '# These are credentials. This file is gitignored — keep it that way.\n\n';
|
|
63
|
+
await writeFile(path, `${header}${body}`);
|
|
64
|
+
return { path, created: true, added: missing.map((parameter) => parameter.name) };
|
|
65
|
+
}
|
|
66
|
+
await appendFile(path, `\n${body}`);
|
|
67
|
+
return { path, created: false, added: missing.map((parameter) => parameter.name) };
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Copia a fonte para onde o `firebase-functions` a lê. Sem isso o valor preenchido na raiz não chega a
|
|
71
|
+
* function nenhuma, e a falha aparece só quando a feature é usada.
|
|
72
|
+
*/
|
|
73
|
+
export const stageOfficeEnv = async (root, projectId, install) => {
|
|
74
|
+
const source = officeEnvPath(root, projectId);
|
|
75
|
+
// Ausente não é erro: o `provision` pode correr numa instalação que ainda não desempacotou nada, e
|
|
76
|
+
// quem avisa que falta valor é a conferência de parâmetros, com a lista do que falta.
|
|
77
|
+
if (!await exists(source))
|
|
78
|
+
return false;
|
|
79
|
+
const target = join(install, 'functions', officeEnvName(projectId));
|
|
80
|
+
await mkdir(dirname(target), { recursive: true });
|
|
81
|
+
await copyFile(source, target);
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
const GITIGNORE_LINES = [
|
|
85
|
+
'# Local Microteia installation: rebuilt from the package plus "microteia connect".',
|
|
86
|
+
'.microteia/',
|
|
87
|
+
'',
|
|
88
|
+
'# Backend settings. These are credentials — never commit them.',
|
|
89
|
+
'.env.*',
|
|
90
|
+
'# Ignore examples, which are not part of the installation and are not meant to be committed.',
|
|
91
|
+
'examples/',
|
|
92
|
+
];
|
|
93
|
+
/** Um padrão que cubra `.env.<projectId>`. Não tenta ser um matcher de gitignore: reconhece os usuais. */
|
|
94
|
+
const coversEnvFiles = (content) => content
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((line) => line.trim())
|
|
97
|
+
.some((line) => line === '.env.*' || line === '.env*' || line === '*.env' || line === '.env');
|
|
98
|
+
/**
|
|
99
|
+
* Garante que o `.env.<projectId>` não vai para um repositório. O diretório é do escritório e tende a
|
|
100
|
+
* ser versionado — é onde ele guarda as extensões que escreve —, e uma credencial commitada por
|
|
101
|
+
* omissão nossa é o pior desfecho possível deste comando.
|
|
102
|
+
*
|
|
103
|
+
* Cria o arquivo quando não existe. Quando existe, ACRESCENTA só a linha que falta e diz que o fez:
|
|
104
|
+
* reescrever o `.gitignore` de alguém seria pior que o problema.
|
|
105
|
+
*/
|
|
106
|
+
export const ensureRootGitignore = async (root) => {
|
|
107
|
+
const path = join(root, '.gitignore');
|
|
108
|
+
if (!await exists(path)) {
|
|
109
|
+
await writeFile(path, `${GITIGNORE_LINES.join('\n')}\n`);
|
|
110
|
+
return 'created';
|
|
111
|
+
}
|
|
112
|
+
const content = await readFile(path, 'utf8');
|
|
113
|
+
if (coversEnvFiles(content))
|
|
114
|
+
return 'ok';
|
|
115
|
+
const separator = content.endsWith('\n') ? '' : '\n';
|
|
116
|
+
await appendFile(path, `${separator}\n# Backend settings. These are credentials — never commit them.\n.env.*\n`);
|
|
117
|
+
return 'appended';
|
|
118
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FirestoreIndexSpec } from '../kernel/backendShape.js';
|
|
2
|
+
export declare const encodeIndex: (index: FirestoreIndexSpec) => string;
|
|
3
|
+
export declare const decodeIndex: (entry: string) => FirestoreIndexSpec | null;
|
|
4
|
+
/**
|
|
5
|
+
* Entrada que não decodifica é DESCARTADA, não fatal. O pior caso de descartar é a memória de um índice
|
|
6
|
+
* se perder, que é exatamente o comportamento anterior a este módulo; o pior caso de lançar é um
|
|
7
|
+
* `microteia.yaml` editado à mão impedir um upgrade — e o arquivo fica ao lado das Cloud Functions do
|
|
8
|
+
* escritório, então ele É editado à mão.
|
|
9
|
+
*/
|
|
10
|
+
export declare const parseIndexState: (raw: string | null) => FirestoreIndexSpec[];
|
|
11
|
+
export interface IndexStateOutcome {
|
|
12
|
+
value: string | null;
|
|
13
|
+
skipped: FirestoreIndexSpec[];
|
|
14
|
+
}
|
|
15
|
+
export declare const serializeIndexState: (indexes: FirestoreIndexSpec[]) => IndexStateOutcome;
|
|
16
|
+
/** Uma linha legível, para o relatório — é ela que o operador lê, não o escalar do `microteia.yaml`. */
|
|
17
|
+
export declare const describeIndex: (index: FirestoreIndexSpec) => string;
|
|
18
|
+
export interface IndexesFile {
|
|
19
|
+
indexes: FirestoreIndexSpec[];
|
|
20
|
+
fieldOverrides?: unknown[];
|
|
21
|
+
}
|
|
22
|
+
export declare const readIndexFile: (install: string) => Promise<IndexesFile>;
|
|
23
|
+
export declare const writeIndexFile: (install: string, file: IndexesFile) => Promise<void>;
|
|
24
|
+
export interface Convergence {
|
|
25
|
+
desired: FirestoreIndexSpec[];
|
|
26
|
+
missing: FirestoreIndexSpec[];
|
|
27
|
+
changed: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `baseline ∪ manter`, na ordem em que chega — reescrever a partir da baseline, e não acrescentar ao que
|
|
31
|
+
* está lá, é o que faz um índice sair do arquivo quando ele deixa de ser pedido E de ser lembrado.
|
|
32
|
+
* `missing` é o que o arquivo ainda não declarava, e é ele que decide se vale chamar o `firebase`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const convergeIndexes: (current: FirestoreIndexSpec[], baseline: FirestoreIndexSpec[], keep: FirestoreIndexSpec[]) => Convergence;
|
|
35
|
+
/**
|
|
36
|
+
* Devolve ao arquivo recém-desempacotado o que a instalação já tinha declarado. É o `unpack` que apaga
|
|
37
|
+
* `install/`, e é ele que repara — antes de qualquer comando poder rodar `firebase deploy` contra um
|
|
38
|
+
* arquivo incompleto. Não toca no Firebase: só reescreve o arquivo.
|
|
39
|
+
*/
|
|
40
|
+
export declare const restoreDeclaredIndexes: (install: string, baseline: FirestoreIndexSpec[], recorded: FirestoreIndexSpec[]) => Promise<FirestoreIndexSpec[]>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { indexKey } from '../kernel/backendShape.js';
|
|
4
|
+
/**
|
|
5
|
+
* Quais índices compostos de EXTENSÃO já foram declarados neste projeto, guardados na raiz.
|
|
6
|
+
*
|
|
7
|
+
* Existe pelo mesmo motivo que `backend.functions`: o estado tem de sobreviver ao `unpack`. O que
|
|
8
|
+
* havia era só o `firestore.indexes.json` de dentro de `.microteia/install/`, que o `unpack` apaga
|
|
9
|
+
* inteiro — e o pacote traz apenas a BASELINE, porque o que a extensão reivindica sai dela no `pack`.
|
|
10
|
+
*
|
|
11
|
+
* O efeito era este, e chegou a acontecer: provisiona-se (o índice da extensão entra no arquivo e é
|
|
12
|
+
* criado no projeto), desativa-se a extensão (o índice fica, e é o certo), troca-se de kernel, e o
|
|
13
|
+
* `microteia deploy` roda `firebase deploy --only firestore:indexes` contra um arquivo que voltou à
|
|
14
|
+
* baseline. O `firebase` acha no projeto índices que o arquivo não declara e PERGUNTA se pode apagar,
|
|
15
|
+
* no meio de um upgrade — e responder "sim" leva embora o índice de uma extensão que ainda pode voltar.
|
|
16
|
+
* Pior: `reportOrphanIndexes` lia a mesma memória apagada, então o aviso que existe para o órfão não
|
|
17
|
+
* passar em branco emudecia exatamente ali.
|
|
18
|
+
*/
|
|
19
|
+
const ENTRY_SEPARATOR = ';';
|
|
20
|
+
const FIELD_SEPARATOR = ',';
|
|
21
|
+
const SCOPE_SEPARATOR = '/';
|
|
22
|
+
const GROUP_SCOPE = 'group';
|
|
23
|
+
/**
|
|
24
|
+
* A marca de ordenação é o ÚLTIMO caractere do campo, e não um separador: é o que permite um campo
|
|
25
|
+
* chamado `order-date` — o `-` no meio não decide nada. `?` é o campo sem ordenação nem `arrayConfig`,
|
|
26
|
+
* que o Firestore recusa no deploy mas o manifesto de uma extensão pode declarar; representá-lo é mais
|
|
27
|
+
* barato que uma exceção, e faz a ida e volta ser exata para todo índice que exista.
|
|
28
|
+
*/
|
|
29
|
+
const MARKS = {
|
|
30
|
+
ASCENDING: '+',
|
|
31
|
+
DESCENDING: '-',
|
|
32
|
+
CONTAINS: '*',
|
|
33
|
+
NONE: '?',
|
|
34
|
+
};
|
|
35
|
+
const encodeField = (field) => {
|
|
36
|
+
if (field.arrayConfig !== undefined && field.order !== undefined)
|
|
37
|
+
return null;
|
|
38
|
+
if (field.arrayConfig !== undefined)
|
|
39
|
+
return `${field.fieldPath}${MARKS.CONTAINS}`;
|
|
40
|
+
if (field.order === 'DESCENDING')
|
|
41
|
+
return `${field.fieldPath}${MARKS.DESCENDING}`;
|
|
42
|
+
if (field.order === 'ASCENDING')
|
|
43
|
+
return `${field.fieldPath}${MARKS.ASCENDING}`;
|
|
44
|
+
return `${field.fieldPath}${MARKS.NONE}`;
|
|
45
|
+
};
|
|
46
|
+
const decodeField = (token) => {
|
|
47
|
+
const path = token.slice(0, -1);
|
|
48
|
+
if (path === '')
|
|
49
|
+
return null;
|
|
50
|
+
switch (token.slice(-1)) {
|
|
51
|
+
case MARKS.CONTAINS: return { fieldPath: path, arrayConfig: 'CONTAINS' };
|
|
52
|
+
case MARKS.DESCENDING: return { fieldPath: path, order: 'DESCENDING' };
|
|
53
|
+
case MARKS.ASCENDING: return { fieldPath: path, order: 'ASCENDING' };
|
|
54
|
+
case MARKS.NONE: return { fieldPath: path };
|
|
55
|
+
default: return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const MARK_VALUES = Object.values(MARKS);
|
|
59
|
+
/**
|
|
60
|
+
* O que a codificação não consegue representar sem ambiguidade. Um nome de coleção não pode conter `/`
|
|
61
|
+
* (o Firestore não permite) e um caminho de campo com `;`, `,` ou `:` só apareceria entre acentos
|
|
62
|
+
* graves — mas "só apareceria" é como um defeito silencioso começa, então aqui ele é DETECTADO e o
|
|
63
|
+
* índice fica de fora da memória com aviso, em vez de voltar do arquivo virado noutro índice.
|
|
64
|
+
*/
|
|
65
|
+
const representable = (index) => {
|
|
66
|
+
const reserved = [ENTRY_SEPARATOR, FIELD_SEPARATOR, ':', SCOPE_SEPARATOR];
|
|
67
|
+
if (reserved.some((token) => index.collectionGroup.includes(token)))
|
|
68
|
+
return false;
|
|
69
|
+
if (index.collectionGroup === '')
|
|
70
|
+
return false;
|
|
71
|
+
return index.fields.every((field) => {
|
|
72
|
+
const encoded = encodeField(field);
|
|
73
|
+
if (encoded === null)
|
|
74
|
+
return false;
|
|
75
|
+
if (reserved.some((token) => field.fieldPath.includes(token)))
|
|
76
|
+
return false;
|
|
77
|
+
return !MARK_VALUES.includes(field.fieldPath.slice(-1));
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
export const encodeIndex = (index) => {
|
|
81
|
+
const scope = index.queryScope === 'COLLECTION_GROUP' ? `${SCOPE_SEPARATOR}${GROUP_SCOPE}` : '';
|
|
82
|
+
const fields = index.fields.map(encodeField).join(FIELD_SEPARATOR);
|
|
83
|
+
return `${index.collectionGroup}${scope}:${fields}`;
|
|
84
|
+
};
|
|
85
|
+
export const decodeIndex = (entry) => {
|
|
86
|
+
const separator = entry.indexOf(':');
|
|
87
|
+
if (separator <= 0)
|
|
88
|
+
return null;
|
|
89
|
+
const head = entry.slice(0, separator);
|
|
90
|
+
const tail = entry.slice(separator + 1);
|
|
91
|
+
if (tail === '')
|
|
92
|
+
return null;
|
|
93
|
+
const group = head.endsWith(`${SCOPE_SEPARATOR}${GROUP_SCOPE}`);
|
|
94
|
+
const collectionGroup = group ? head.slice(0, -(GROUP_SCOPE.length + 1)) : head;
|
|
95
|
+
if (collectionGroup === '' || collectionGroup.includes(SCOPE_SEPARATOR))
|
|
96
|
+
return null;
|
|
97
|
+
const fields = tail.split(FIELD_SEPARATOR).map(decodeField);
|
|
98
|
+
if (fields.some((field) => field === null))
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
collectionGroup,
|
|
102
|
+
queryScope: group ? 'COLLECTION_GROUP' : 'COLLECTION',
|
|
103
|
+
fields: fields,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Entrada que não decodifica é DESCARTADA, não fatal. O pior caso de descartar é a memória de um índice
|
|
108
|
+
* se perder, que é exatamente o comportamento anterior a este módulo; o pior caso de lançar é um
|
|
109
|
+
* `microteia.yaml` editado à mão impedir um upgrade — e o arquivo fica ao lado das Cloud Functions do
|
|
110
|
+
* escritório, então ele É editado à mão.
|
|
111
|
+
*/
|
|
112
|
+
export const parseIndexState = (raw) => {
|
|
113
|
+
const indexes = new Map();
|
|
114
|
+
for (const entry of (raw ?? '').split(ENTRY_SEPARATOR)) {
|
|
115
|
+
const trimmed = entry.trim();
|
|
116
|
+
if (trimmed === '')
|
|
117
|
+
continue;
|
|
118
|
+
const index = decodeIndex(trimmed);
|
|
119
|
+
if (index !== null)
|
|
120
|
+
indexes.set(indexKey(index), index);
|
|
121
|
+
}
|
|
122
|
+
return [...indexes.values()];
|
|
123
|
+
};
|
|
124
|
+
export const serializeIndexState = (indexes) => {
|
|
125
|
+
const entries = new Map();
|
|
126
|
+
const skipped = [];
|
|
127
|
+
for (const index of indexes) {
|
|
128
|
+
if (!representable(index)) {
|
|
129
|
+
skipped.push(index);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
entries.set(indexKey(index), encodeIndex(index));
|
|
133
|
+
}
|
|
134
|
+
const value = [...entries.values()].sort();
|
|
135
|
+
return { value: value.length === 0 ? null : value.join(ENTRY_SEPARATOR), skipped };
|
|
136
|
+
};
|
|
137
|
+
/** Uma linha legível, para o relatório — é ela que o operador lê, não o escalar do `microteia.yaml`. */
|
|
138
|
+
export const describeIndex = (index) => {
|
|
139
|
+
const scope = index.queryScope === 'COLLECTION_GROUP' ? ' (collection group)' : '';
|
|
140
|
+
return `${index.collectionGroup}${scope}: ${index.fields.map((field) => field.fieldPath).join(', ')}`;
|
|
141
|
+
};
|
|
142
|
+
const indexFilePath = (install) => join(install, 'firestore.indexes.json');
|
|
143
|
+
export const readIndexFile = async (install) => JSON.parse(await readFile(indexFilePath(install), 'utf8'));
|
|
144
|
+
export const writeIndexFile = async (install, file) => {
|
|
145
|
+
await writeFile(indexFilePath(install), `${JSON.stringify(file, null, 2)}\n`);
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* `baseline ∪ manter`, na ordem em que chega — reescrever a partir da baseline, e não acrescentar ao que
|
|
149
|
+
* está lá, é o que faz um índice sair do arquivo quando ele deixa de ser pedido E de ser lembrado.
|
|
150
|
+
* `missing` é o que o arquivo ainda não declarava, e é ele que decide se vale chamar o `firebase`.
|
|
151
|
+
*/
|
|
152
|
+
export const convergeIndexes = (current, baseline, keep) => {
|
|
153
|
+
const desired = [];
|
|
154
|
+
const present = new Set();
|
|
155
|
+
for (const index of [...baseline, ...keep]) {
|
|
156
|
+
const key = indexKey(index);
|
|
157
|
+
if (present.has(key))
|
|
158
|
+
continue;
|
|
159
|
+
present.add(key);
|
|
160
|
+
desired.push(index);
|
|
161
|
+
}
|
|
162
|
+
const declared = new Set(current.map(indexKey));
|
|
163
|
+
const missing = desired.filter((index) => !declared.has(indexKey(index)));
|
|
164
|
+
return { desired, missing, changed: missing.length > 0 || current.length !== desired.length };
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Devolve ao arquivo recém-desempacotado o que a instalação já tinha declarado. É o `unpack` que apaga
|
|
168
|
+
* `install/`, e é ele que repara — antes de qualquer comando poder rodar `firebase deploy` contra um
|
|
169
|
+
* arquivo incompleto. Não toca no Firebase: só reescreve o arquivo.
|
|
170
|
+
*/
|
|
171
|
+
export const restoreDeclaredIndexes = async (install, baseline, recorded) => {
|
|
172
|
+
const file = await readIndexFile(install);
|
|
173
|
+
const convergence = convergeIndexes(file.indexes, baseline, recorded);
|
|
174
|
+
if (!convergence.changed)
|
|
175
|
+
return [];
|
|
176
|
+
await writeIndexFile(install, { ...file, indexes: convergence.desired });
|
|
177
|
+
return convergence.missing;
|
|
178
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BackendClaim } from '../kernel/backendShape.js';
|
|
2
|
+
/**
|
|
3
|
+
* O valor de um parâmetro de backend é carimbado no deploy de CADA function: o `firebase-functions`
|
|
4
|
+
* resolve os `defineString` e grava o resultado como variável de ambiente da function que está
|
|
5
|
+
* subindo. Quem preenche o `.env.<projectId>` DEPOIS da instalação não alcança function nenhuma — ela
|
|
6
|
+
* continua rodando com a string vazia que o `ensureOfficeEnv` escreveu, e a falha aparece só no runtime
|
|
7
|
+
* da feature.
|
|
8
|
+
*
|
|
9
|
+
* O provisionamento não notava isso porque o estado que ele guardava era só QUAIS functions subiram,
|
|
10
|
+
* nunca COM QUAIS VALORES. Este módulo é a outra metade: uma impressão digital por parâmetro no
|
|
11
|
+
* `microteia.yaml`, comparada a cada execução. Parâmetro que mudou torna obsoletas as functions da
|
|
12
|
+
* extensão que o declara, e elas voltam para a fila de deploy mesmo já estando aplicadas.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Os parâmetros PREENCHIDOS do arquivo de ambiente. Linha sem valor é ausência: é assim que o
|
|
16
|
+
* `ensureOfficeEnv` grava o que ainda falta, e tratá-la como valor faria toda instalação nova nascer
|
|
17
|
+
* com uma mudança pendente.
|
|
18
|
+
*/
|
|
19
|
+
export declare const parseBackendEnv: (content: string) => Map<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Lê a FONTE: o `.env.<projectId>` ao lado do `microteia.yaml`. A cópia dentro de
|
|
22
|
+
* `.microteia/install/functions/` existe só porque é de lá que o `firebase-functions` lê — comparar
|
|
23
|
+
* contra ela seria comparar contra o que o último deploy carimbou, não contra o que a pessoa digitou.
|
|
24
|
+
*/
|
|
25
|
+
export declare const readBackendEnv: (root: string, projectId: string) => Promise<Map<string, string>>;
|
|
26
|
+
/**
|
|
27
|
+
* A impressão digital é SALGADA com o projeto e o nome do parâmetro. O `microteia.yaml` fica ao lado
|
|
28
|
+
* das Cloud Functions do escritório e não guarda credencial: um sha256 puro de um valor curto seria
|
|
29
|
+
* uma consulta de dicionário, e um salgado não é. Doze hexadecimais bastam para detectar mudança e
|
|
30
|
+
* não reconstroem valor nenhum.
|
|
31
|
+
*/
|
|
32
|
+
export declare const parameterDigest: (projectId: string, name: string, value: string) => string;
|
|
33
|
+
export declare const parameterDigests: (projectId: string, values: Map<string, string>, tracked: string[]) => Map<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* `NOME:digest` separado por vírgula, no mesmo escalar de `backend.functions` e pelo mesmo motivo: o
|
|
36
|
+
* leitor de YAML daqui entende mapas de dois níveis e escalares, e trazer sequências para guardar meia
|
|
37
|
+
* dúzia de pares seria pagar caro pela generalidade.
|
|
38
|
+
*/
|
|
39
|
+
export declare const parseParameterState: (raw: string | null) => Map<string, string>;
|
|
40
|
+
export declare const serializeParameterState: (state: Map<string, string>) => string | null;
|
|
41
|
+
/**
|
|
42
|
+
* O que mudou entre o que foi aplicado e o que está no arquivo agora — incluindo o parâmetro que
|
|
43
|
+
* ESVAZIOU, porque a function deployada continua carregando o valor antigo e convergir é reescrevê-la
|
|
44
|
+
* com o que o arquivo diz.
|
|
45
|
+
*/
|
|
46
|
+
export declare const changedParameters: (recorded: Map<string, string>, current: Map<string, string>, tracked: string[]) => string[];
|
|
47
|
+
/** As functions das extensões que declaram algum destes parâmetros. */
|
|
48
|
+
export declare const functionsDeclaringParameters: (claims: BackendClaim[], names: string[]) => string[];
|