@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.
- package/CONSOLE-VERSIONS.md +12 -0
- package/README.md +1 -1
- package/lib/console/pluginWorkspace.js +93 -27
- package/lib/index.js +1 -1
- package/lib/shared/paths.d.ts +1 -1
- package/lib/shared/paths.js +1 -1
- package/lib/vendor/pluginWorkspaceTemplates.d.ts +18 -1
- package/lib/vendor/pluginWorkspaceTemplates.js +30 -9
- package/package.json +1 -1
package/CONSOLE-VERSIONS.md
CHANGED
|
@@ -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
|
@@ -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
|
-
/**
|
|
112
|
-
|
|
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
|
|
116
|
-
return
|
|
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
|
|
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
|
-
*
|
|
124
|
-
*
|
|
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
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
|
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,
|
package/lib/shared/paths.d.ts
CHANGED
|
@@ -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.
|
|
90
|
+
export declare const CLI_VERSION = "2.43.0";
|
package/lib/shared/paths.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
//
|
|
30
|
-
// o
|
|
31
|
-
|
|
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
|
+
};
|