@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.
Files changed (85) hide show
  1. package/CONSOLE-VERSIONS.md +85 -0
  2. package/README.md +64 -0
  3. package/lib/console/backendEnv.d.ts +52 -0
  4. package/lib/console/backendEnv.js +118 -0
  5. package/lib/console/backendIndexes.d.ts +40 -0
  6. package/lib/console/backendIndexes.js +178 -0
  7. package/lib/console/backendParameters.d.ts +48 -0
  8. package/lib/console/backendParameters.js +100 -0
  9. package/lib/console/callableAccess.d.ts +39 -0
  10. package/lib/console/callableAccess.js +61 -0
  11. package/lib/console/channel.d.ts +49 -0
  12. package/lib/console/channel.js +145 -0
  13. package/lib/console/connect.d.ts +2 -0
  14. package/lib/console/connect.js +74 -0
  15. package/lib/console/customFunctions.d.ts +120 -0
  16. package/lib/console/customFunctions.js +298 -0
  17. package/lib/console/deploy.d.ts +2 -0
  18. package/lib/console/deploy.js +261 -0
  19. package/lib/console/deployFunctions.d.ts +45 -0
  20. package/lib/console/deployFunctions.js +204 -0
  21. package/lib/console/environment.d.ts +62 -0
  22. package/lib/console/environment.js +151 -0
  23. package/lib/console/firebaseCli.d.ts +15 -0
  24. package/lib/console/firebaseCli.js +90 -0
  25. package/lib/console/firestoreRest.d.ts +39 -0
  26. package/lib/console/firestoreRest.js +131 -0
  27. package/lib/console/hostingCheck.d.ts +29 -0
  28. package/lib/console/hostingCheck.js +44 -0
  29. package/lib/console/init.d.ts +2 -0
  30. package/lib/console/init.js +60 -0
  31. package/lib/console/pluginWorkspace.d.ts +48 -0
  32. package/lib/console/pluginWorkspace.js +148 -0
  33. package/lib/console/provision.d.ts +70 -0
  34. package/lib/console/provision.js +478 -0
  35. package/lib/console/regionOrphans.d.ts +43 -0
  36. package/lib/console/regionOrphans.js +99 -0
  37. package/lib/console/runtimeConfigShape.d.ts +25 -0
  38. package/lib/console/runtimeConfigShape.js +35 -0
  39. package/lib/console/unpack.d.ts +9 -0
  40. package/lib/console/unpack.js +333 -0
  41. package/lib/console/versions.d.ts +10 -0
  42. package/lib/console/versions.js +72 -0
  43. package/lib/console/workspace.d.ts +19 -0
  44. package/lib/console/workspace.js +33 -0
  45. package/lib/index.d.ts +2 -0
  46. package/lib/index.js +136 -0
  47. package/lib/kernel/backendShape.d.ts +74 -0
  48. package/lib/kernel/backendShape.js +91 -0
  49. package/lib/kernel/channel.d.ts +79 -0
  50. package/lib/kernel/channel.js +157 -0
  51. package/lib/kernel/manifest.d.ts +138 -0
  52. package/lib/kernel/manifest.js +145 -0
  53. package/lib/shared/args.d.ts +14 -0
  54. package/lib/shared/args.js +44 -0
  55. package/lib/shared/digest.d.ts +30 -0
  56. package/lib/shared/digest.js +34 -0
  57. package/lib/shared/paths.d.ts +90 -0
  58. package/lib/shared/paths.js +97 -0
  59. package/lib/shared/run.d.ts +28 -0
  60. package/lib/shared/run.js +49 -0
  61. package/lib/shared/ui.d.ts +23 -0
  62. package/lib/shared/ui.js +43 -0
  63. package/lib/vendor/authoringGuide.d.ts +19 -0
  64. package/lib/vendor/authoringGuide.js +284 -0
  65. package/lib/vendor/bundleAudit.d.ts +19 -0
  66. package/lib/vendor/bundleAudit.js +95 -0
  67. package/lib/vendor/changelogs.d.ts +62 -0
  68. package/lib/vendor/changelogs.js +131 -0
  69. package/lib/vendor/exampleSources.d.ts +29 -0
  70. package/lib/vendor/exampleSources.js +126 -0
  71. package/lib/vendor/installationDocs.d.ts +35 -0
  72. package/lib/vendor/installationDocs.js +243 -0
  73. package/lib/vendor/pack.d.ts +2 -0
  74. package/lib/vendor/pack.js +276 -0
  75. package/lib/vendor/packConsole.d.ts +13 -0
  76. package/lib/vendor/packConsole.js +63 -0
  77. package/lib/vendor/packageTemplates.d.ts +66 -0
  78. package/lib/vendor/packageTemplates.js +200 -0
  79. package/lib/vendor/pluginWorkspaceTemplates.d.ts +19 -0
  80. package/lib/vendor/pluginWorkspaceTemplates.js +29 -0
  81. package/lib/vendor/publish.d.ts +2 -0
  82. package/lib/vendor/publish.js +277 -0
  83. package/lib/vendor/repository.d.ts +9 -0
  84. package/lib/vendor/repository.js +26 -0
  85. package/package.json +21 -0
@@ -0,0 +1,276 @@
1
+ import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { basename, join, relative } from 'node:path';
3
+ import { flagValue, hasFlag } from '../shared/args.js';
4
+ import { repoRoot } from '../shared/paths.js';
5
+ import { run } from '../shared/run.js';
6
+ import { blank, bullets, heading, info, step, success } from '../shared/ui.js';
7
+ import { DEFAULT_FUNCTIONS_REGION, KERNEL_COMPONENTS, KERNEL_DISTRIBUTION_ID, MANUAL_PREREQUISITES, MINIMUM_CLI_VERSION, REQUIRED_NODE_VERSION, callableFunctionNames, declaredParametersIn, exportedFunctionNames, parseKernelVersion, } from '../kernel/manifest.js';
8
+ import { digestOfFile } from '../shared/digest.js';
9
+ import { kernelTarballName } from '../kernel/channel.js';
10
+ import { baselineFunctionsOf, baselineIndexesOf, claimProblems, claimsFromTemplates, } from '../kernel/backendShape.js';
11
+ import { declarationsWithComments, environmentSecrets, findBundleLeaks } from './bundleAudit.js';
12
+ import { KERNEL_CHANGELOG, readChangelog } from './changelogs.js';
13
+ import { assertInsideRepository } from './repository.js';
14
+ import { SDK_TARBALL } from './pluginWorkspaceTemplates.js';
15
+ import { stageExamples } from './exampleSources.js';
16
+ import { packageCustomIndexTs, packageCustomTsconfig, packageFirebaseJson, packageFunctionsIndexJs, packageFunctionsPackageJson, } from './packageTemplates.js';
17
+ /**
18
+ * `dist` é um modo do Vite para o qual NÃO existe `.env.dist`. É isso que garante um bundle sem
19
+ * projeto embutido: o repositório não tem `.env` de raiz servindo de fallback, então nada é inlinado.
20
+ */
21
+ const DISTRIBUTION_TARGET = 'dist';
22
+ /**
23
+ * As exigências de backend das extensões de PRIMEIRA PARTE saem dos `extension-templates/*.json`, que
24
+ * são gerados dos `.ts` e conferidos pelo `verify-extensions` que o gate de qualidade acima já roda.
25
+ * Ler o `.json` gerado em vez do `.ts` evita carregar o Vite aqui só para avaliar um módulo.
26
+ */
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 readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
34
+ /**
35
+ * Os `defineString` e as callables estão espalhados pelos handlers, não num arquivo só — então a
36
+ * varredura é a árvore inteira de `functions/src`, uma vez, para os dois. Derivar daqui é o que faz
37
+ * um parâmetro novo (ou uma callable nova) aparecer sozinho no manifesto, em vez de depender de
38
+ * alguém lembrar de atualizar um template.
39
+ */
40
+ const scanBackendSources = async (directory) => {
41
+ const parameters = new Map();
42
+ const callables = new Set();
43
+ const walk = async (current) => {
44
+ for (const entry of await readdir(current, { withFileTypes: true })) {
45
+ const path = join(current, entry.name);
46
+ if (entry.isDirectory()) {
47
+ await walk(path);
48
+ }
49
+ else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
50
+ const source = await readFile(path, 'utf8');
51
+ for (const parameter of declaredParametersIn(source)) {
52
+ const previous = parameters.get(parameter.name);
53
+ // Declarado em dois lugares com defaults diferentes: vale o mais exigente.
54
+ if (previous === undefined || parameter.required)
55
+ parameters.set(parameter.name, parameter);
56
+ }
57
+ for (const name of callableFunctionNames(source))
58
+ callables.add(name);
59
+ }
60
+ }
61
+ };
62
+ await walk(directory);
63
+ return {
64
+ parameters: [...parameters.values()].sort((left, right) => left.name.localeCompare(right.name)),
65
+ callables: [...callables].sort((left, right) => left.localeCompare(right)),
66
+ };
67
+ };
68
+ /**
69
+ * Põe o SDK de autoria no pacote: o compilado, os tipos da API curada, e as extensões de primeira
70
+ * parte como exemplo.
71
+ *
72
+ * O tarball vai com nome FIXO (`microteia-sdk.tgz`, sem versão) porque o `extensions/package.json` do
73
+ * escritório aponta para ele por caminho: com a versão no nome, todo upgrade exigiria editar aquele
74
+ * arquivo à mão, e quem esquecesse continuaria compilando contra o SDK antigo em silêncio.
75
+ *
76
+ * As três conferências do fim são do ARTEFATO, não do procedimento — mesma postura do `bundleAudit`:
77
+ * o que sai daqui é `.d.ts` da superfície curada, e um comentário esquecido nele levaria para todo
78
+ * escritório a narrativa interna de uma decisão nossa.
79
+ */
80
+ const stageSdk = async (root, stageDir, version) => {
81
+ const sourceDir = join(root, 'sdk');
82
+ const targetDir = join(stageDir, 'sdk');
83
+ const manifest = await readJson(join(sourceDir, 'package.json'));
84
+ const kernelVersion = manifest.microteia?.kernelVersion;
85
+ if (manifest.version !== version || kernelVersion === undefined) {
86
+ throw new Error('sdk/package.json is stale — "npm run verify" should have rebuilt it.');
87
+ }
88
+ await mkdir(targetDir, { recursive: true });
89
+ await run('npm', ['pack', '--silent', '--pack-destination', targetDir], { cwd: sourceDir });
90
+ const packed = (await readdir(targetDir)).filter((file) => file.endsWith('.tgz'));
91
+ if (packed.length !== 1) {
92
+ throw new Error(`Expected exactly one SDK tarball, found ${packed.length}.`);
93
+ }
94
+ await rename(join(targetDir, packed[0]), join(targetDir, SDK_TARBALL));
95
+ await cp(join(sourceDir, 'types'), join(targetDir, 'types'), { recursive: true });
96
+ // O `.d.ts` da superfície curada sai SEM comentário: o JSDoc deste repositório é em português e
97
+ // narra decisões e incidentes internos. O que o autor lê é `types/` e o `AUTHORING.md`, escritos
98
+ // para ele — e por isso `types/` fica de fora desta conferência, de propósito.
99
+ const documented = await declarationsWithComments(join(sourceDir, 'lib'));
100
+ if (documented.length > 0) {
101
+ throw new Error('Declarations in the SDK carry comments, which would ship internal notes to every office:\n'
102
+ + documented.map((file) => ` ${relative(sourceDir, file)}`).join('\n')
103
+ + '\n"removeComments" must stay on in tsconfig.sdk.json.');
104
+ }
105
+ // As extensões de primeira parte como material de autoria, em FONTE e uma pasta por extensão. O
106
+ // `.json` é o artefato e não ensina como se chega nele; a fonte ensina, e já viajava de qualquer
107
+ // forma — o código das views está dentro do `.json`, em string. `exampleSources.test.ts` compila cada
108
+ // uma e exige o `.json` versionado byte a byte.
109
+ const examples = await stageExamples(root, join(targetDir, 'examples'));
110
+ info(` ${examples.length} first-party extension(s) staged as source examples`);
111
+ return { version, kernelVersion, path: `sdk/${SDK_TARBALL}` };
112
+ };
113
+ export const pack = async (args) => {
114
+ await assertInsideRepository('pack');
115
+ const root = repoRoot;
116
+ const skipVerify = hasFlag(args, 'skip-verify');
117
+ const pkg = await readJson(join(root, 'package.json'));
118
+ const version = pkg.version;
119
+ /**
120
+ * O histórico técnico é conferido AQUI, antes de qualquer build. Uma entrada que falta é um minuto de
121
+ * conserto; descobri-la depois do `verify` e dos três builds custa dez, e é assim que a conferência
122
+ * passa a ser contornada com `--skip-verify`.
123
+ */
124
+ const changelog = await readChangelog(root, KERNEL_CHANGELOG, version);
125
+ const kernelVersion = parseKernelVersion(await readFile(join(root, 'src/domain/schema/plugin/kernelVersion.ts'), 'utf8'));
126
+ const functionNames = exportedFunctionNames(await readFile(join(root, 'functions/src/index.ts'), 'utf8'));
127
+ const scan = await scanBackendSources(join(root, 'functions/src'));
128
+ const backendParameters = scan.parameters;
129
+ // Uma callable que não está exportada no `index.ts` não é deployada, e verificá-la acusaria uma
130
+ // ausência que é só do parser.
131
+ const callables = scan.callables.filter((name) => functionNames.includes(name));
132
+ const rootIndexes = await readJson(join(root, 'firestore.indexes.json'));
133
+ const claims = await firstPartyClaims(root);
134
+ const problems = claimProblems(claims, functionNames, rootIndexes.indexes);
135
+ if (problems.length > 0) {
136
+ throw new Error('An extension declares backend artifacts that do not add up, so the package would be wrong:\n'
137
+ + problems.map((problem) => ` ${problem}`).join('\n'));
138
+ }
139
+ const baselineFunctions = baselineFunctionsOf(functionNames, claims);
140
+ const baselineIndexes = baselineIndexesOf(rootIndexes.indexes, claims);
141
+ const outputDir = flagValue(args, 'output') ?? join(root, 'distributions', 'kernel', version);
142
+ const stageDir = join(outputDir, 'package');
143
+ const tarball = join(outputDir, kernelTarballName(version));
144
+ heading(`Packaging Microteia kernel ${version} (contract ${kernelVersion})`);
145
+ const total = 7;
146
+ if (skipVerify) {
147
+ info('Quality gate skipped (--skip-verify).');
148
+ }
149
+ else {
150
+ step(1, total, 'Quality gate');
151
+ await run('npm', ['run', 'verify'], { cwd: root });
152
+ }
153
+ step(2, total, `Building the portable frontend (TARGET=${DISTRIBUTION_TARGET})`);
154
+ await run('npm', ['run', 'build'], { cwd: root, env: { TARGET: DISTRIBUTION_TARGET } });
155
+ step(3, total, 'Compiling the Cloud Functions');
156
+ await run('npm', ['run', 'build:functions'], { cwd: root });
157
+ // Depois do `verify`, que já compilou o SDK — mas o `--skip-verify` existe, e um pacote sem SDK
158
+ // seria descoberto só no `plugin-init` de um escritório.
159
+ step(4, total, 'Building the authoring SDK');
160
+ await run('npm', ['run', 'build:host-types'], { cwd: root });
161
+ await run('npm', ['run', 'build:sdk'], { cwd: root });
162
+ step(5, total, 'Checking that the bundle carries no Firebase project');
163
+ const leaks = await findBundleLeaks(join(root, 'dist'), await environmentSecrets(root));
164
+ if (leaks.length > 0) {
165
+ throw new Error('The built frontend contains Firebase credentials, so it is not distributable.\n'
166
+ + leaks.map((leak) => ` ${leak.file}: ${leak.what}`).join('\n')
167
+ + `\nThe distribution build must run with TARGET=${DISTRIBUTION_TARGET}, for which no env file exists.`);
168
+ }
169
+ success('no project baked into the bundle');
170
+ step(6, total, 'Assembling the distribution');
171
+ await rm(stageDir, { recursive: true, force: true });
172
+ await mkdir(stageDir, { recursive: true });
173
+ const repoFirebase = await readJson(join(root, 'firebase.json'));
174
+ if (repoFirebase.hosting === undefined)
175
+ throw new Error('firebase.json has no hosting section.');
176
+ const repoFunctions = await readJson(join(root, 'functions/package.json'));
177
+ // `.DS_Store` e afins vêm de `public/` e entram no `dist` pela cópia literal que o Vite faz. O
178
+ // hosting já os ignoraria no upload, mas eles não têm por que existir dentro do pacote.
179
+ await cp(join(root, 'dist'), join(stageDir, 'hosting'), {
180
+ recursive: true,
181
+ filter: (source) => !basename(source).startsWith('.'),
182
+ });
183
+ // Os `.js.map` ficam de fora: sem o `src/` que eles apontam são inúteis para quem instala, e
184
+ // publicá-los entregaria a árvore interna de módulos do kernel — o desenho, ainda que não o
185
+ // código. (`sourcesContent` não é emitido, então o TypeScript em si nunca esteve neles.)
186
+ await cp(join(root, 'functions/lib'), join(stageDir, 'functions/lib'), {
187
+ recursive: true,
188
+ filter: (source) => !source.endsWith('.map'),
189
+ });
190
+ await cp(join(root, 'functions/package-lock.json'), join(stageDir, 'functions/package-lock.json'));
191
+ // O `.env.example` NÃO viaja. Dentro de `install/functions/` ele era uma armadilha: convidava a
192
+ // preencher a configuração ali, num diretório que o deploy seguinte apaga por inteiro. O que a
193
+ // instalação recebe é o `.env.<projectId>` na RAIZ, escrito a partir do manifesto e já comentado —
194
+ // um modelo a menos é um lugar errado a menos.
195
+ for (const file of ['firestore.rules', 'storage.rules']) {
196
+ await cp(join(root, file), join(stageDir, file));
197
+ }
198
+ // O histórico do kernel viaja DENTRO do pacote, e é de lá que o `unpack` o copia para a raiz da
199
+ // instalação. Escrito do conteúdo já conferido, para não ler o arquivo duas vezes.
200
+ await writeFile(join(stageDir, KERNEL_CHANGELOG), changelog);
201
+ // O `firestore.indexes.json` do pacote é ESCRITO, não copiado: a raiz é o conjunto completo (é o que
202
+ // roda em `microteia-dev` e `curatino`), e o que uma extensão reivindica sai daqui para ser criado
203
+ // pelo `provision` quando ela for instalada. Os `fieldOverrides` são todos do kernel e vão inteiros.
204
+ await writeFile(join(stageDir, 'firestore.indexes.json'), `${JSON.stringify({ ...rootIndexes, indexes: baselineIndexes }, null, 2)}\n`);
205
+ await mkdir(join(stageDir, 'functions/custom/src'), { recursive: true });
206
+ await writeFile(join(stageDir, 'firebase.json'), packageFirebaseJson(repoFirebase.hosting));
207
+ await writeFile(join(stageDir, 'functions/package.json'), packageFunctionsPackageJson(repoFunctions, version));
208
+ await writeFile(join(stageDir, 'functions/index.js'), packageFunctionsIndexJs());
209
+ await writeFile(join(stageDir, 'functions/custom/tsconfig.json'), packageCustomTsconfig());
210
+ await writeFile(join(stageDir, 'functions/custom/src/index.ts'), packageCustomIndexTs());
211
+ const sdk = await stageSdk(root, stageDir, version);
212
+ const manifest = {
213
+ id: KERNEL_DISTRIBUTION_ID,
214
+ name: 'Microteia Kernel',
215
+ version,
216
+ kernelVersion,
217
+ minimumCliVersion: MINIMUM_CLI_VERSION,
218
+ packagedAt: new Date().toISOString(),
219
+ components: KERNEL_COMPONENTS,
220
+ requires: {
221
+ functionsRegion: DEFAULT_FUNCTIONS_REGION,
222
+ nodeVersion: REQUIRED_NODE_VERSION,
223
+ },
224
+ functions: functionNames,
225
+ baselineFunctions,
226
+ baselineIndexes,
227
+ callables,
228
+ backendParameters,
229
+ manualPrerequisites: MANUAL_PREREQUISITES,
230
+ sdk,
231
+ };
232
+ await writeFile(join(stageDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
233
+ step(7, total, 'Writing the tarball');
234
+ await run('tar', ['-czf', tarball, '-C', stageDir, '.'], { cwd: root });
235
+ await rm(stageDir, { recursive: true, force: true });
236
+ /**
237
+ * A segunda cópia do manifesto, FORA do tarball, para que a versão e os pré-requisitos possam ser
238
+ * lidos sem desempacotar nada — e agora também para que o que foi BAIXADO possa ser conferido.
239
+ *
240
+ * Ela deixou de ser o mesmo JSON da de dentro, e vem depois do `tar` por causa disso: o `sha256` é do
241
+ * arquivo que acabou de ser escrito, e a cópia de dentro dele não teria como carregá-lo. É a única
242
+ * diferença entre as duas, e está declarada no tipo.
243
+ */
244
+ const digest = await digestOfFile(tarball);
245
+ const published = {
246
+ ...manifest,
247
+ tarball: { name: basename(tarball), ...digest },
248
+ };
249
+ await writeFile(join(outputDir, 'manifest.json'), `${JSON.stringify(published, null, 2)}\n`);
250
+ blank();
251
+ info(`Kernel ${version} packaged.`);
252
+ bullets([
253
+ `tarball: ${tarball}`,
254
+ `manifest: ${join(outputDir, 'manifest.json')}`,
255
+ `functions: ${functionNames.length} packaged, ${baselineFunctions.length} in the baseline`,
256
+ `callables: ${callables.length}`,
257
+ `claimed by extensions: ${functionNames.length - baselineFunctions.length} function(s), `
258
+ + `${rootIndexes.indexes.length - baselineIndexes.length} index(es), from ${claims.length} extension(s)`,
259
+ `backend parameters: ${backendParameters.length}`,
260
+ `authoring SDK: ${sdk.version} (kernel contract ${sdk.kernelVersion})`,
261
+ `size: ${(digest.bytes / 1_000_000).toFixed(1)} MB, sha256 ${digest.sha256.slice(0, 12)}…`,
262
+ ]);
263
+ blank();
264
+ info('Publish it with:');
265
+ bullets(['microteia publish --dry-run (says what would go up)', 'microteia publish']);
266
+ blank();
267
+ info('Or install this file directly, without publishing:');
268
+ bullets([
269
+ 'microteia init --name <office>',
270
+ 'microteia connect --project <firebase-project-id>',
271
+ `microteia unpack --kernel ${tarball}`,
272
+ 'microteia deploy',
273
+ 'microteia plugin-init (optional: author extensions in TypeScript)',
274
+ ]);
275
+ blank();
276
+ };
@@ -0,0 +1,13 @@
1
+ import type { ParsedArgs } from '../shared/args.js';
2
+ /**
3
+ * O Console pronto para rodar em outra pasta — ou em outra máquina — sem o repositório.
4
+ *
5
+ * O que ele monta é pequeno de propósito: o `lib/` compilado, um `package.json` e um README. Não há
6
+ * dependência de npm a instalar porque o Console usa só módulos nativos do Node; o que ele exige são
7
+ * BINÁRIOS (`firebase`, `npm`, `tar`, `gcloud`), e esses são da máquina, não do pacote.
8
+ *
9
+ * O `package.json` é a razão de este comando existir em vez de um `cp -R`: o compilado é ESM, e sem
10
+ * `type: module` o Node só o carrega por adivinhação de sintaxe — disponível a partir do 22.7 e
11
+ * ausente antes dele. Montar a pasta à mão significa esquecer disso e culpar a ferramenta.
12
+ */
13
+ export declare const packConsole: (args: ParsedArgs) => Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { access, chmod, cp, mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { flagValue } from '../shared/args.js';
4
+ import { CLI_VERSION, consoleRoot, repoRoot } from '../shared/paths.js';
5
+ import { blank, bullets, heading, info, step, success } from '../shared/ui.js';
6
+ import { CONSOLE_CHANGELOG, readChangelog } from './changelogs.js';
7
+ import { consolePackageJson, consoleReadme } from './packageTemplates.js';
8
+ import { assertInsideRepository } from './repository.js';
9
+ /**
10
+ * O Console pronto para rodar em outra pasta — ou em outra máquina — sem o repositório.
11
+ *
12
+ * O que ele monta é pequeno de propósito: o `lib/` compilado, um `package.json` e um README. Não há
13
+ * dependência de npm a instalar porque o Console usa só módulos nativos do Node; o que ele exige são
14
+ * BINÁRIOS (`firebase`, `npm`, `tar`, `gcloud`), e esses são da máquina, não do pacote.
15
+ *
16
+ * O `package.json` é a razão de este comando existir em vez de um `cp -R`: o compilado é ESM, e sem
17
+ * `type: module` o Node só o carrega por adivinhação de sintaxe — disponível a partir do 22.7 e
18
+ * ausente antes dele. Montar a pasta à mão significa esquecer disso e culpar a ferramenta.
19
+ */
20
+ export const packConsole = async (args) => {
21
+ await assertInsideRepository('pack-console');
22
+ const compiled = join(repoRoot, 'cli', 'lib');
23
+ const outputDir = flagValue(args, 'output')
24
+ ?? join(repoRoot, 'distributions', 'console', CLI_VERSION);
25
+ heading(`Packaging Microteia Console ${CLI_VERSION}`);
26
+ const total = 3;
27
+ step(1, total, 'Checking the compiled Console');
28
+ try {
29
+ await access(join(compiled, 'index.js'));
30
+ }
31
+ catch {
32
+ throw new Error(`${compiled} has no compiled Console. Run "npm run build:cli" first `
33
+ + '(or use "npm run pack:console", which does it).');
34
+ }
35
+ success(`${compiled}/index.js`);
36
+ /**
37
+ * O histórico do Console viaja COM o Console, e não com o pacote do kernel: é o único jeito de ele não
38
+ * poder descrever uma versão que não está ali. A conferência vem antes de montar a pasta, porque uma
39
+ * entrada que falta se escreve em um minuto e nada mais depende dela.
40
+ */
41
+ step(2, total, 'Checking the changelog');
42
+ const changelog = await readChangelog(consoleRoot, CONSOLE_CHANGELOG, CLI_VERSION);
43
+ success(`${CONSOLE_CHANGELOG} covers ${CLI_VERSION}`);
44
+ step(3, total, 'Assembling the folder');
45
+ await rm(outputDir, { recursive: true, force: true });
46
+ await mkdir(outputDir, { recursive: true });
47
+ await cp(compiled, join(outputDir, 'lib'), { recursive: true });
48
+ await writeFile(join(outputDir, 'package.json'), consolePackageJson(CLI_VERSION));
49
+ await writeFile(join(outputDir, 'README.md'), consoleReadme(CLI_VERSION));
50
+ await writeFile(join(outputDir, CONSOLE_CHANGELOG), changelog);
51
+ // Executável com o shebang que o entrypoint já tem: é o que permite apontar um link para ele em
52
+ // qualquer diretório do PATH, sem npm e sem sudo. O `tsc` não carimba a permissão, e no repositório
53
+ // ela vem do `npm install` (que a aplica no alvo do `bin`) — o que não existe numa pasta copiada.
54
+ await chmod(join(outputDir, 'lib', 'index.js'), 0o755);
55
+ success(outputDir);
56
+ blank();
57
+ info('Publish it with:');
58
+ bullets(['microteia publish --console-only --dry-run', 'microteia publish --console-only']);
59
+ blank();
60
+ info('Or run this folder as it is, on any machine with Node 22 and the Firebase CLI:');
61
+ bullets([`node ${join(outputDir, 'lib', 'index.js')} --help`]);
62
+ blank();
63
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * O que é GERADO dentro do pacote — o que difere de um workspace de deploy do repositório.
3
+ *
4
+ * O `firebase.json` do repositório não serve como está por duas razões: o `predeploy` dele compila o
5
+ * kernel (que no pacote já vem compilado, e cujo `src/` não viaja), e o hosting dele aponta para
6
+ * `dist`. Aqui o `predeploy` compila SÓ o que o escritório escreveu.
7
+ */
8
+ export interface RepoFunctionsPackage {
9
+ dependencies?: Record<string, string>;
10
+ devDependencies?: Record<string, string>;
11
+ engines?: Record<string, string>;
12
+ }
13
+ export interface HostingConfig {
14
+ ignore: string[];
15
+ rewrites: unknown[];
16
+ headers: unknown[];
17
+ }
18
+ export declare const packageFirebaseJson: (hosting: HostingConfig) => string;
19
+ /**
20
+ * As dependências são copiadas INTACTAS do `functions/package.json` do repositório, para que o
21
+ * `package-lock.json` que viaja junto continue válido. O que muda é só o entrypoint (passa a ser o
22
+ * `index.js` que soma kernel e custom) e o `build`, que agora compila apenas `custom/`.
23
+ */
24
+ /**
25
+ * A `version` não é enfeite: sem ela o npm não consegue formar `nome@versão` e todo aviso dele sobre
26
+ * este pacote sai com `package: undefined`, o que faz parecer que o arquivo está corrompido.
27
+ *
28
+ * `engines.node` vem do repositório e descreve o RUNTIME das Cloud Functions — é por ele que o Firebase
29
+ * escolhe em qual Node a function roda. Ele não descreve a máquina de quem instala, e é por isso que o
30
+ * npm avisa `EBADENGINE` quando as duas versões diferem: o aviso é esperado e não impede nada. Trocar
31
+ * o valor por uma faixa (`>=22`) calaria o aviso e passaria a deixar a escolha do runtime ambígua para
32
+ * o Firebase — o preço errado pela linha de saída.
33
+ */
34
+ export declare const packageFunctionsPackageJson: (source: RepoFunctionsPackage, version: string) => string;
35
+ /**
36
+ * Entrypoint da instalação. O espalhamento mantém os nomes PLANOS (`onUserCreated`, e não
37
+ * `kernel-onUserCreated`), que é o que os deploys por nome e os gatilhos já existentes esperam.
38
+ */
39
+ export declare const packageFunctionsIndexJs: () => string;
40
+ /**
41
+ * O `exclude` é cinto de segurança, não redundância: o `gatherCustomFunctions` já não copia teste para
42
+ * cá, mas ele reconhece as duas convenções usuais (`*.test.ts`, `__tests__/`) e um arquivo fora delas
43
+ * chegaria. Aqui ele chega e não compila — o que é um upload maior, e não um deploy quebrado.
44
+ */
45
+ export declare const packageCustomTsconfig: () => string;
46
+ export declare const packageCustomIndexTs: () => string;
47
+ export declare const CONSOLE_PACKAGE_NAME = "@microteia/console";
48
+ /**
49
+ * O Console empacotado para rodar FORA do repositório — e agora publicável.
50
+ *
51
+ * O `type: module` não é detalhe: o compilado é ESM, e sem esta declaração o Node só o carrega por
52
+ * adivinhação de sintaxe — que existe a partir do 22.7 e falha em silêncio nas versões anteriores, com
53
+ * "Cannot use import statement outside a module". O `bin` é o que dá o comando `microteia` global.
54
+ *
55
+ * Ele deixou de ser `private`, e isso resolve um ovo-e-galinha: enquanto o Console também tinha de ser
56
+ * baixado de algum lugar, alguém precisava baixar o baixador — e o primeiro passo de quem instala era
57
+ * uma pasta recebida por e-mail e um `ln -s` escrito à mão. Publicado, o registry já dá URL versionada,
58
+ * impressão digital no lock, CDN e `npm outdated`, que era tudo o que o canal precisaria reimplementar.
59
+ *
60
+ * `files` é a lista exata, e o `CONSOLE-VERSIONS.md` está nela por um motivo que não é documentação: o
61
+ * `unpack` lê aquele arquivo AO LADO do binário que está rodando para pô-lo na raiz da instalação. Fora
62
+ * de `files`, ele não existe numa instalação global, e o comando passa a avisar que o Console não carrega
63
+ * histórico — na máquina do escritório, e só lá.
64
+ */
65
+ export declare const consolePackageJson: (version: string) => string;
66
+ export declare const consoleReadme: (version: string) => string;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * O que é GERADO dentro do pacote — o que difere de um workspace de deploy do repositório.
3
+ *
4
+ * O `firebase.json` do repositório não serve como está por duas razões: o `predeploy` dele compila o
5
+ * kernel (que no pacote já vem compilado, e cujo `src/` não viaja), e o hosting dele aponta para
6
+ * `dist`. Aqui o `predeploy` compila SÓ o que o escritório escreveu.
7
+ */
8
+ export const packageFirebaseJson = (hosting) => JSON.stringify({
9
+ firestore: {
10
+ rules: 'firestore.rules',
11
+ indexes: 'firestore.indexes.json',
12
+ },
13
+ storage: {
14
+ rules: 'storage.rules',
15
+ },
16
+ functions: {
17
+ source: 'functions',
18
+ predeploy: ['npm --prefix "$RESOURCE_DIR" run build'],
19
+ },
20
+ hosting: {
21
+ public: 'hosting',
22
+ ignore: hosting.ignore,
23
+ rewrites: hosting.rewrites,
24
+ headers: hosting.headers,
25
+ },
26
+ }, null, 2) + '\n';
27
+ /**
28
+ * As dependências são copiadas INTACTAS do `functions/package.json` do repositório, para que o
29
+ * `package-lock.json` que viaja junto continue válido. O que muda é só o entrypoint (passa a ser o
30
+ * `index.js` que soma kernel e custom) e o `build`, que agora compila apenas `custom/`.
31
+ */
32
+ /**
33
+ * A `version` não é enfeite: sem ela o npm não consegue formar `nome@versão` e todo aviso dele sobre
34
+ * este pacote sai com `package: undefined`, o que faz parecer que o arquivo está corrompido.
35
+ *
36
+ * `engines.node` vem do repositório e descreve o RUNTIME das Cloud Functions — é por ele que o Firebase
37
+ * escolhe em qual Node a function roda. Ele não descreve a máquina de quem instala, e é por isso que o
38
+ * npm avisa `EBADENGINE` quando as duas versões diferem: o aviso é esperado e não impede nada. Trocar
39
+ * o valor por uma faixa (`>=22`) calaria o aviso e passaria a deixar a escolha do runtime ambígua para
40
+ * o Firebase — o preço errado pela linha de saída.
41
+ */
42
+ export const packageFunctionsPackageJson = (source, version) => JSON.stringify({
43
+ name: 'microteia-functions',
44
+ version,
45
+ private: true,
46
+ main: 'index.js',
47
+ engines: source.engines ?? { node: '22' },
48
+ scripts: {
49
+ build: 'tsc -p custom/tsconfig.json',
50
+ },
51
+ dependencies: source.dependencies ?? {},
52
+ devDependencies: source.devDependencies ?? {},
53
+ }, null, 2) + '\n';
54
+ /**
55
+ * Entrypoint da instalação. O espalhamento mantém os nomes PLANOS (`onUserCreated`, e não
56
+ * `kernel-onUserCreated`), que é o que os deploys por nome e os gatilhos já existentes esperam.
57
+ */
58
+ export const packageFunctionsIndexJs = () => `// Generated by "microteia pack". Do not edit.
59
+ //
60
+ // The deployed function set is the compiled kernel plus whatever this installation added under
61
+ // custom/. Kernel functions cannot be overridden here: custom/ is spread last, so a name collision
62
+ // would silently replace a kernel trigger — keep your function names distinct.
63
+ const kernel = require('./lib/index.js');
64
+ const custom = require('./custom/lib/index.js');
65
+
66
+ module.exports = { ...kernel, ...custom };
67
+ `;
68
+ /**
69
+ * O `exclude` é cinto de segurança, não redundância: o `gatherCustomFunctions` já não copia teste para
70
+ * cá, mas ele reconhece as duas convenções usuais (`*.test.ts`, `__tests__/`) e um arquivo fora delas
71
+ * chegaria. Aqui ele chega e não compila — o que é um upload maior, e não um deploy quebrado.
72
+ */
73
+ export const packageCustomTsconfig = () => JSON.stringify({
74
+ compilerOptions: {
75
+ module: 'commonjs',
76
+ moduleResolution: 'node',
77
+ target: 'es2022',
78
+ lib: ['ES2022'],
79
+ types: ['node'],
80
+ rootDir: 'src',
81
+ outDir: 'lib',
82
+ strict: true,
83
+ esModuleInterop: true,
84
+ skipLibCheck: true,
85
+ sourceMap: true,
86
+ noImplicitReturns: true,
87
+ },
88
+ include: ['src'],
89
+ exclude: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', '**/__tests__/**'],
90
+ }, null, 2) + '\n';
91
+ export const packageCustomIndexTs = () => `// Cloud Functions belonging to this installation.
92
+ //
93
+ // Everything exported from this file is deployed alongside the Microteia kernel and survives a
94
+ // kernel upgrade — "microteia deploy" replaces lib/ but never touches custom/.
95
+ //
96
+ // Functions must be pinned to the same region as the rest of the installation, otherwise Firestore
97
+ // triggers will not fire. Example:
98
+ //
99
+ // import { onCall } from 'firebase-functions/v2/https';
100
+ //
101
+ // export const helloOffice = onCall({ region: 'southamerica-east1', cors: true }, () => {
102
+ // return { ok: true };
103
+ // });
104
+
105
+ export {};
106
+ `;
107
+ export const CONSOLE_PACKAGE_NAME = '@microteia/console';
108
+ /**
109
+ * O Console empacotado para rodar FORA do repositório — e agora publicável.
110
+ *
111
+ * O `type: module` não é detalhe: o compilado é ESM, e sem esta declaração o Node só o carrega por
112
+ * adivinhação de sintaxe — que existe a partir do 22.7 e falha em silêncio nas versões anteriores, com
113
+ * "Cannot use import statement outside a module". O `bin` é o que dá o comando `microteia` global.
114
+ *
115
+ * Ele deixou de ser `private`, e isso resolve um ovo-e-galinha: enquanto o Console também tinha de ser
116
+ * baixado de algum lugar, alguém precisava baixar o baixador — e o primeiro passo de quem instala era
117
+ * uma pasta recebida por e-mail e um `ln -s` escrito à mão. Publicado, o registry já dá URL versionada,
118
+ * impressão digital no lock, CDN e `npm outdated`, que era tudo o que o canal precisaria reimplementar.
119
+ *
120
+ * `files` é a lista exata, e o `CONSOLE-VERSIONS.md` está nela por um motivo que não é documentação: o
121
+ * `unpack` lê aquele arquivo AO LADO do binário que está rodando para pô-lo na raiz da instalação. Fora
122
+ * de `files`, ele não existe numa instalação global, e o comando passa a avisar que o Console não carrega
123
+ * histórico — na máquina do escritório, e só lá.
124
+ */
125
+ export const consolePackageJson = (version) => JSON.stringify({
126
+ name: CONSOLE_PACKAGE_NAME,
127
+ version,
128
+ description: 'Install and update Microteia in a Firebase project you own.',
129
+ license: 'UNLICENSED',
130
+ type: 'module',
131
+ bin: { microteia: 'lib/index.js' },
132
+ files: ['lib', 'README.md', 'CONSOLE-VERSIONS.md'],
133
+ engines: { node: '>=22' },
134
+ publishConfig: { access: 'public' },
135
+ }, null, 2) + '\n';
136
+ export const consoleReadme = (version) => `# Microteia Console ${version}
137
+
138
+ Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
139
+ what it needs are command-line tools that are already on the machine, listed at the bottom.
140
+
141
+ ## Installing it
142
+
143
+ npm i -g ${CONSOLE_PACKAGE_NAME}
144
+ microteia --help
145
+
146
+ Or run it without installing anything:
147
+
148
+ npx ${CONSOLE_PACKAGE_NAME} --help
149
+
150
+ Where npm's global prefix belongs to root — Node installed system-wide under \`/usr/local\` — the global
151
+ install fails with EACCES. Point npm at a prefix of your own instead of reaching for sudo, which would
152
+ write root-owned files that then need sudo to remove:
153
+
154
+ npm config set prefix ~/.npm-global
155
+ export PATH="$HOME/.npm-global/bin:$PATH" # add to ~/.zshrc
156
+
157
+ This folder also runs as it is, with no npm at all:
158
+
159
+ node lib/index.js --help
160
+
161
+ ## Using it
162
+
163
+ microteia init --name "<office>"
164
+ microteia connect --project <your-firebase-project-id>
165
+ microteia unpack --version latest
166
+ microteia deploy
167
+
168
+ \`connect\` prints everything that has to be prepared in the Firebase project first — start with the
169
+ Blaze plan, since nothing else can be created without it.
170
+
171
+ \`unpack --version\` reads the published catalog, downloads the version and checks its sha256 before
172
+ touching what is installed. On a machine that cannot reach the catalog, name a file you already have:
173
+
174
+ microteia unpack --kernel <microteia-kernel-x.y.z>.tgz
175
+
176
+ \`microteia --version\` answers what is installed here, and says so when a newer kernel is published.
177
+
178
+ Later, whenever an office installs an extension that needs server-side resources, run:
179
+
180
+ microteia provision
181
+
182
+ The extension shows "Backend pending" in the application until you do, and stays out of the menu.
183
+
184
+ ## What has to exist on this machine
185
+
186
+ | Tool | Used for |
187
+ | ------------- | -------------------------------------------------------------- |
188
+ | Node.js 22+ | running this Console (newer is fine; the functions' own runtime is pinned to 22) |
189
+ | \`firebase\` | every deploy — authenticate with \`firebase login\` first |
190
+ | \`npm\`, \`tar\` | installing the functions' dependencies and unpacking the kernel |
191
+ | \`gcloud\` | creating the App Engine application, and reading which extensions an office installed (\`gcloud auth login\`) |
192
+
193
+ The \`pack\` and \`publish\` commands in here build and release the platform itself, and only work inside
194
+ the Microteia repository.
195
+
196
+ ## What changed in this Console
197
+
198
+ \`CONSOLE-VERSIONS.md\`, in this folder. \`unpack\` copies it into the installation it manages, beside the
199
+ platform's own \`KERNEL-VERSIONS.md\` — each history travels with the artifact it describes.
200
+ `;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * O pouco que o CONSOLE sabe sobre `extensions/`.
3
+ *
4
+ * Tudo o que descreve o kernel — `AGENTS.md`, `AUTHORING.md`, `tsconfig.json`, `.mcp.json`, os scripts
5
+ * — morava aqui, e era o lado errado. O Console é versionado (`CLI_VERSION`) e distribuído SEPARADAMENTE
6
+ * do kernel: um Console de duas versões atrás ao lado de uma instalação nova escrevia, em silêncio, a
7
+ * documentação de outro contrato. Passou para `src/sdk/node/workspace/`, que viaja no mesmo `.tgz` que
8
+ * os pontos de extensão que o texto descreve, e é reescrito a cada troca de kernel.
9
+ *
10
+ * Sobrou o que só o Console tem como saber: onde está o tarball do SDK dentro do pacote, e o nome do
11
+ * escritório. É a semente — mínima de propósito, porque quem a completa é o `sync`.
12
+ */
13
+ export declare const SDK_TARBALL = "microteia-sdk.tgz";
14
+ /**
15
+ * A semente do `package.json`. Não traz scripts nem devDependencies: os nomes de comando são do SDK, e
16
+ * duplicá-los aqui era exatamente o problema. O `sync` os acrescenta logo depois do `npm install`, e o
17
+ * segundo `npm install` os instala.
18
+ */
19
+ export declare const workspacePackageJson: (officeName: string, sdkPath: string) => string;