@microteia/console 2.42.0 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,18 @@
22
22
  - fix(command): what was broken, and what it did to whoever hit it.
23
23
  -->
24
24
 
25
+ #### [2.43.0] — 2026-11-10
26
+
27
+ - feat(plugin-init): with a terminal, asks the publisher and the prefix right after the namespace, each
28
+ with a short explanation, and records them in the seed `package.json` under `microteia.publisher` and
29
+ `microteia.prefix` — the keys `microteia-plugin create` reads. The publisher suggests the office name
30
+ (`project.name` in `microteia.yaml`); the prefix suggests the one `create` would (the namespace's last
31
+ segment when it fits, `com.acme` → `acme`); Enter accepts either. Until now both were asked by the first
32
+ `create`, so a value that never changes was chosen by whoever ran the first extension — often an agent.
33
+ - feat(plugin-init): `--publisher <name>` and `--prefix <acme>` answer without a terminal; an invalid
34
+ prefix is refused before anything is written. Without a terminal and without the flags, both are left
35
+ out and `create` asks for them once, as before. `--force` reuses the three values already recorded.
36
+
25
37
  #### [2.42.0] — 2026-11-08
26
38
 
27
39
  - feat(kernel/manifest)!: the optional prerequisite `self-service-tenants` and the backend parameter
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Microteia Console 2.42.0
1
+ # Microteia Console 2.43.0
2
2
 
3
3
  Installs and updates Microteia in a Firebase project you own. It has no npm dependencies of its own —
4
4
  what it needs are command-line tools that are already on the machine, listed at the bottom.
@@ -6,7 +6,7 @@ import { blank, bullets, heading, info, success, warn } from '../shared/ui.js';
6
6
  import { run } from '../shared/run.js';
7
7
  import { ENVIRONMENT_FILE, readEnvironment } from './environment.js';
8
8
  import { WORKSPACE_DIR } from './workspace.js';
9
- import { SDK_TARBALL, isValidNamespace, workspacePackageJson } from '../vendor/pluginWorkspaceTemplates.js';
9
+ import { SDK_TARBALL, isValidNamespace, isValidPrefix, suggestPrefix, workspacePackageJson, } from '../vendor/pluginWorkspaceTemplates.js';
10
10
  /**
11
11
  * `microteia plugin-init` — prepara `extensions/`, onde o escritório escreve extensão em TypeScript.
12
12
  *
@@ -108,40 +108,100 @@ export const refreshAuthoringWorkspace = async (root, officeName) => {
108
108
  return false;
109
109
  }
110
110
  };
111
- /** O que um `--force` anterior deixou gravado: reescrever a semente não é motivo para perguntar de novo. */
112
- const existingNamespace = async (directory) => {
111
+ /**
112
+ * O que um `--force` anterior deixou gravado: reescrever a semente não é motivo para perguntar de novo —
113
+ * e o prefixo, uma vez usado, não pode mudar.
114
+ */
115
+ const existingIdentity = async (directory) => {
113
116
  try {
114
117
  const pkg = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
115
- const value = pkg.microteia?.namespace;
116
- return typeof value === 'string' && isValidNamespace(value) ? value : null;
118
+ const { namespace, publisher, prefix } = pkg.microteia ?? {};
119
+ return {
120
+ ...(typeof namespace === 'string' && isValidNamespace(namespace) ? { namespace } : {}),
121
+ ...(typeof publisher === 'string' && publisher.trim() !== '' ? { publisher: publisher.trim() } : {}),
122
+ ...(typeof prefix === 'string' && isValidPrefix(prefix) ? { prefix } : {}),
123
+ };
117
124
  }
118
125
  catch {
119
- return null;
126
+ return {};
120
127
  }
121
128
  };
129
+ const invalidNamespace = (namespace) => new Error(`"${namespace}" is not a valid namespace. Use reverse-DNS, lowercase, from a domain your organization`
130
+ + ' controls — for example: --namespace com.acme');
131
+ const invalidPrefix = (prefix) => new Error(`"${prefix}" is not a valid prefix. Use 2 to 8 lowercase letters or digits, starting with a letter`
132
+ + ' — for example: --prefix acme');
133
+ /** Com sugestão, Enter a aceita — é o que torna a sugestão uma resposta, e não só um exemplo. */
134
+ const asker = (rl) => async (label, suggestion) => {
135
+ const answer = (await rl.question(suggestion === undefined ? `${label}: ` : `${label} [${suggestion}]: `)).trim();
136
+ return answer === '' && suggestion !== undefined ? suggestion : answer;
137
+ };
122
138
  /**
123
- * O namespace é OBRIGATÓRIO, e não tem padrão: qualquer padrão seria um prefixo que não é do escritório,
124
- * e o id que nasce dele é permanente. Sem terminal, só a flag responde.
139
+ * Namespace, publisher e prefix — os três valem para TODA extensão do diretório, e por isso são
140
+ * perguntados aqui, uma vez, e não no primeiro `create`: quem chegava a ele de um agente via o prefixo
141
+ * ser escolhido por quem não podia escolhê-lo.
142
+ *
143
+ * O namespace é OBRIGATÓRIO e não tem padrão: qualquer padrão seria um prefixo que não é do escritório,
144
+ * e o id que nasce dele é permanente. Sem terminal, só a flag o responde. Publisher e prefix, sem
145
+ * terminal e sem flag, ficam para o `create` — que já sabe perguntá-los.
125
146
  */
126
- const resolveNamespace = async (args, directory) => {
127
- const given = flagValue(args, 'namespace') ?? await existingNamespace(directory) ?? await askNamespace();
128
- const namespace = given.trim();
129
- if (!isValidNamespace(namespace)) {
130
- throw new Error(`"${namespace}" is not a valid namespace. Use reverse-DNS, lowercase, from a domain your organization`
131
- + ' controls — for example: --namespace com.acme');
132
- }
133
- return namespace;
134
- };
135
- const askNamespace = async () => {
147
+ const resolveIdentity = async (args, directory, officeName) => {
148
+ const stored = await existingIdentity(directory);
149
+ const given = {
150
+ namespace: flagValue(args, 'namespace')?.trim() ?? stored.namespace,
151
+ publisher: flagValue(args, 'publisher')?.trim() ?? stored.publisher,
152
+ prefix: flagValue(args, 'prefix')?.trim() ?? stored.prefix,
153
+ };
154
+ if (given.namespace !== undefined && !isValidNamespace(given.namespace))
155
+ throw invalidNamespace(given.namespace);
156
+ if (given.prefix !== undefined && !isValidPrefix(given.prefix))
157
+ throw invalidPrefix(given.prefix);
158
+ if (given.publisher === '')
159
+ throw new Error('The publisher cannot be empty — for example: --publisher "Acme Legal"');
136
160
  if (!process.stdin.isTTY) {
137
- throw new Error('A namespace is required: every extension written in this directory is named below it, and its ids\n'
138
- + 'are permanent. Pass it with --namespace, for example: microteia plugin-init --namespace com.acme');
161
+ if (given.namespace === undefined) {
162
+ throw new Error('A namespace is required: every extension written in this directory is named below it, and its ids\n'
163
+ + 'are permanent. Pass it with --namespace, for example: microteia plugin-init --namespace com.acme');
164
+ }
165
+ return { namespace: given.namespace, publisher: given.publisher, prefix: given.prefix };
166
+ }
167
+ if (given.namespace !== undefined && given.publisher !== undefined && given.prefix !== undefined) {
168
+ return { namespace: given.namespace, publisher: given.publisher, prefix: given.prefix };
139
169
  }
140
- info('Every extension written here is named below a namespace — the reverse-DNS of a domain your');
141
- info('organization controls, like com.acme. Its ids are permanent, so choose one that is yours.');
142
170
  const rl = createInterface({ input: process.stdin, output: process.stdout });
171
+ const ask = asker(rl);
143
172
  try {
144
- return (await rl.question('namespace: ')).trim();
173
+ let namespace = given.namespace;
174
+ if (namespace === undefined) {
175
+ blank();
176
+ info('NAMESPACE — every extension written here is named below it: "billing" becomes com.acme.billing.');
177
+ info('Use the reverse-DNS of a domain your organization controls. The ids are permanent, so choose one');
178
+ info('that is yours.');
179
+ namespace = await ask('namespace');
180
+ if (!isValidNamespace(namespace))
181
+ throw invalidNamespace(namespace);
182
+ }
183
+ let publisher = given.publisher;
184
+ if (publisher === undefined) {
185
+ blank();
186
+ info('PUBLISHER — who answers for these extensions. It is shown as the author of each one on the');
187
+ info('Extensions screen of every office that installs it. A name, not an id.');
188
+ publisher = await ask('publisher', officeName);
189
+ if (publisher === '')
190
+ throw new Error('The publisher cannot be empty.');
191
+ }
192
+ let prefix = given.prefix;
193
+ if (prefix === undefined) {
194
+ blank();
195
+ info('PREFIX — starts every name your extensions create: acme_clients for a collection, acme-notify for');
196
+ info('an automation. It keeps two publishers who both pick "clients" from colliding in the same office.');
197
+ info('2 to 8 lowercase letters or digits. It never changes: a new prefix would rename the collections');
198
+ info('and leave their records behind. The suggestion comes from the namespace.');
199
+ prefix = await ask('prefix', suggestPrefix(namespace));
200
+ if (!isValidPrefix(prefix))
201
+ throw invalidPrefix(prefix);
202
+ }
203
+ blank();
204
+ return { namespace, publisher, prefix };
145
205
  }
146
206
  finally {
147
207
  rl.close();
@@ -160,19 +220,25 @@ export const pluginInit = async (args) => {
160
220
  }
161
221
  const sdk = await locateSdk(root);
162
222
  const installDir = join(root, WORKSPACE_DIR, 'install');
163
- const namespace = await resolveNamespace(args, directory);
164
223
  heading(`Extension authoring for "${environment.project.name}"`);
224
+ const identity = await resolveIdentity(args, directory, environment.project.name);
225
+ const { namespace } = identity;
165
226
  await mkdir(directory, { recursive: true });
166
227
  // O caminho do tarball é relativo, para que o diretório continue válido se a instalação inteira for
167
228
  // movida — o que acontece assim que alguém a versiona num repositório do escritório.
168
- await writeFile(join(directory, 'package.json'), workspacePackageJson(environment.environment.id, relative(directory, sdk.path), namespace));
169
- success(`wrote the seed package.json (namespace ${namespace})`);
229
+ await writeFile(join(directory, 'package.json'), workspacePackageJson(environment.environment.id, relative(directory, sdk.path), identity));
230
+ success(`wrote the seed package.json (namespace ${namespace}`
231
+ + `${identity.publisher === undefined ? '' : `, publisher "${identity.publisher}"`}`
232
+ + `${identity.prefix === undefined ? '' : `, prefix ${identity.prefix}`})`);
233
+ if (identity.publisher === undefined || identity.prefix === undefined) {
234
+ info(' publisher and prefix not given — "npm run create" asks for them once, and records them there.');
235
+ }
170
236
  blank();
171
237
  info('Installing @microteia/sdk and writing the guides...');
172
238
  await syncAuthoringWorkspace(directory, sdk.path, installDir, environment.project.name);
173
239
  blank();
174
240
  info(`Extension authoring ready in ${relative(root, directory)}/ (kernel contract ${sdk.kernelVersion}).`);
175
- info(`Every extension created there is ${namespace}.<name> — recorded in package.json, under microteia.namespace.`);
241
+ info(`Every extension created there is ${namespace}.<name> — recorded in package.json, under microteia.`);
176
242
  bullets([
177
243
  `cd ${relative(process.cwd(), directory) || '.'}`,
178
244
  'npm run create',
package/lib/index.js CHANGED
@@ -67,7 +67,7 @@ const COMMANDS = [
67
67
  },
68
68
  {
69
69
  name: 'plugin-init',
70
- usage: 'microteia plugin-init --namespace <com.acme> [--dir <path>] [--force]',
70
+ usage: 'microteia plugin-init --namespace <com.acme> [--publisher <name>] [--prefix <acme>] [--dir <path>] [--force]',
71
71
  summary: 'Prepare "extensions/", to author extensions in TypeScript in your editor.',
72
72
  audience: 'office',
73
73
  execute: pluginInit,
@@ -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.42.0";
90
+ export declare const CLI_VERSION = "2.43.0";
@@ -94,4 +94,4 @@ export const consoleRoot = resolve(here, '..', '..');
94
94
  * de código nunca chegou a quem instala. Este bloco fica sendo o que ele já era antes de crescer: a
95
95
  * política de semver da ferramenta, mais o registro do que motivou as versões até a `2.5.0`.
96
96
  */
97
- export const CLI_VERSION = '2.42.0';
97
+ export const CLI_VERSION = '2.43.0';
@@ -16,5 +16,22 @@ export declare const SDK_TARBALL = "microteia-sdk.tgz";
16
16
  * duplicá-los aqui era exatamente o problema. O `sync` os acrescenta logo depois do `npm install`, e o
17
17
  * segundo `npm install` os instala.
18
18
  */
19
- export declare const workspacePackageJson: (officeName: string, sdkPath: string, namespace: string) => string;
19
+ /**
20
+ * O que só quem roda o `plugin-init` pode decidir, e que vale para TODA extensão do diretório. O
21
+ * `publisher` e o `prefix` são opcionais: sem terminal e sem flag, quem os pergunta é o `create` do SDK,
22
+ * uma vez — o mesmo que faz num diretório anterior a eles.
23
+ */
24
+ export interface WorkspaceIdentity {
25
+ namespace: string;
26
+ publisher?: string;
27
+ prefix?: string;
28
+ }
29
+ export declare const workspacePackageJson: (officeName: string, sdkPath: string, identity: WorkspaceIdentity) => string;
20
30
  export declare const isValidNamespace: (value: string) => boolean;
31
+ export declare const isValidPrefix: (value: string) => boolean;
32
+ /**
33
+ * A sugestão do `create` do SDK (`suggestedPrefix`), repetida aqui para que o `plugin-init` ofereça o
34
+ * MESMO prefixo que o `create` ofereceria: o último segmento do namespace, se ele já serve
35
+ * (`com.acme` → `acme`), senão os primeiros caracteres que servem, senão quatro letras sorteadas.
36
+ */
37
+ export declare const suggestPrefix: (namespace: string, random?: () => number) => string;
@@ -11,12 +11,7 @@
11
11
  * escritório. É a semente — mínima de propósito, porque quem a completa é o `sync`.
12
12
  */
13
13
  export 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 const workspacePackageJson = (officeName, sdkPath, namespace) => JSON.stringify({
14
+ export const workspacePackageJson = (officeName, sdkPath, identity) => JSON.stringify({
20
15
  name: `${officeName}-extensions`,
21
16
  version: '0.0.0',
22
17
  private: true,
@@ -26,9 +21,14 @@ export const workspacePackageJson = (officeName, sdkPath, namespace) => JSON.str
26
21
  // traz o SDK novo é `npm run sdk:refresh` — ou o `microteia unpack`, que o faz sozinho.
27
22
  '@microteia/sdk': `file:${sdkPath}`,
28
23
  },
29
- // O prefixo de todo id escrito neste diretório. É o SDK que o lê (`microteia-plugin create`, o MCP);
30
- // o Console só o recebe de quem roda o `plugin-init`, que é a única pessoa que pode decidi-lo.
31
- microteia: { namespace },
24
+ // É o SDK que lê estas chaves (`microteia-plugin create`, o MCP); o Console só as recebe de quem roda
25
+ // o `plugin-init`, que é a única pessoa que pode decidi-las. Chave sem valor fica de fora, e não
26
+ // vazia: o `create` só pergunta o que não acha.
27
+ microteia: {
28
+ namespace: identity.namespace,
29
+ ...(identity.publisher === undefined ? {} : { publisher: identity.publisher }),
30
+ ...(identity.prefix === undefined ? {} : { prefix: identity.prefix }),
31
+ },
32
32
  }, null, 2) + '\n';
33
33
  /**
34
34
  * O formato de id de extensão (`isValidPluginId`, no domínio) — repetido porque o Console não importa
@@ -37,3 +37,24 @@ export const workspacePackageJson = (officeName, sdkPath, namespace) => JSON.str
37
37
  */
38
38
  const NAMESPACE_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
39
39
  export const isValidNamespace = (value) => NAMESPACE_PATTERN.test(value);
40
+ /**
41
+ * O formato do prefixo de artefato (`ARTIFACT_PREFIX_PATTERN`, no domínio) — repetido pela mesma razão do
42
+ * namespace, e preso a ele pelo mesmo teste.
43
+ */
44
+ const PREFIX_PATTERN = /^[a-z][a-z0-9]{1,7}$/;
45
+ const PREFIX_MAX_LENGTH = 8;
46
+ const GENERATED_PREFIX_LENGTH = 4;
47
+ const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
48
+ export const isValidPrefix = (value) => PREFIX_PATTERN.test(value);
49
+ /**
50
+ * A sugestão do `create` do SDK (`suggestedPrefix`), repetida aqui para que o `plugin-init` ofereça o
51
+ * MESMO prefixo que o `create` ofereceria: o último segmento do namespace, se ele já serve
52
+ * (`com.acme` → `acme`), senão os primeiros caracteres que servem, senão quatro letras sorteadas.
53
+ */
54
+ export const suggestPrefix = (namespace, random = Math.random) => {
55
+ const last = (namespace.split('.').pop() ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
56
+ const candidate = last.replace(/^[0-9]+/, '').slice(0, PREFIX_MAX_LENGTH);
57
+ if (isValidPrefix(candidate))
58
+ return candidate;
59
+ return Array.from({ length: GENERATED_PREFIX_LENGTH }, () => LETTERS[Math.floor(random() * LETTERS.length) % LETTERS.length]).join('');
60
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microteia/console",
3
- "version": "2.42.0",
3
+ "version": "2.43.0",
4
4
  "description": "Install and update Microteia in a Firebase project you own.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "repository": {