biaws 0.2.1 → 0.2.2
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/README.md +4 -1
- package/docs/command-taxonomy.md +1 -1
- package/docs/instance-lifecycle.md +40 -0
- package/docs/releasing.md +7 -3
- package/package.json +1 -1
- package/src/cli/admin/instance/update.js +1 -0
- package/src/commands/agent.js +40 -38
- package/src/commands/instance/update.js +164 -0
- package/src/commands/instance.js +1 -0
- package/src/instance/service.js +83 -1
package/README.md
CHANGED
|
@@ -131,7 +131,9 @@ biaws workspace skills status
|
|
|
131
131
|
|
|
132
132
|
O Codex usa `.codex/config.toml` e `.agents/skills`. O Claude Code usa
|
|
133
133
|
`.mcp.json` e `.claude/skills`. Configurações de terceiros são preservadas e
|
|
134
|
-
nenhuma credencial é gravada no projeto.
|
|
134
|
+
nenhuma credencial é gravada no projeto. O MCP é configurado como
|
|
135
|
+
`npx --yes biaws-mcp@<versão-fixada>`, sem exigir um checkout local da
|
|
136
|
+
plataforma.
|
|
135
137
|
|
|
136
138
|
## Monitoramento
|
|
137
139
|
|
|
@@ -171,6 +173,7 @@ biaws admin instance list
|
|
|
171
173
|
biaws admin instance status local
|
|
172
174
|
biaws admin instance start local
|
|
173
175
|
biaws admin instance stop local
|
|
176
|
+
biaws admin instance update local
|
|
174
177
|
biaws admin instance backup local
|
|
175
178
|
```
|
|
176
179
|
|
package/docs/command-taxonomy.md
CHANGED
|
@@ -10,7 +10,7 @@ biaws
|
|
|
10
10
|
│ ├── install
|
|
11
11
|
│ ├── doctor
|
|
12
12
|
│ ├── config
|
|
13
|
-
│ ├── instance setup|list|show|status|start|stop|backup|restore|remove
|
|
13
|
+
│ ├── instance setup|list|show|status|start|stop|update|backup|restore|remove
|
|
14
14
|
│ └── monitoring build|validate|start|stop|status|logs|provision
|
|
15
15
|
├── config
|
|
16
16
|
│ ├── init|login|show|set|unset|doctor
|
|
@@ -35,6 +35,46 @@ biaws admin instance stop local
|
|
|
35
35
|
`status` invocam Docker Compose sem shell e com projeto, arquivo Compose e env
|
|
36
36
|
da instância resolvidos explicitamente.
|
|
37
37
|
|
|
38
|
+
## Atualização
|
|
39
|
+
|
|
40
|
+
Depois de atualizar o checkout ou instalar uma nova release, reconstrua API,
|
|
41
|
+
UI e os demais serviços com:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
biaws admin instance update local
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Antes de alterar a instância, consulte a versão efetivamente implantada e a
|
|
48
|
+
versão disponível no checkout:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
biaws admin instance update local --check
|
|
52
|
+
biaws admin instance update local --check --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
O resultado informa `currentVersion`, `newVersion` e `updateRequired`.
|
|
56
|
+
Instâncias criadas antes desse mecanismo começam com `currentVersion` igual a
|
|
57
|
+
`unknown` e são consideradas pendentes. Se as versões forem iguais, o comando
|
|
58
|
+
não cria backup nem reconstrói containers; use `--force` para repetir o deploy.
|
|
59
|
+
|
|
60
|
+
Por padrão, o comando valida o arquivo Compose, solicita uma senha, cria um
|
|
61
|
+
backup completo e então executa `up -d --build --wait`. Em automação, use um
|
|
62
|
+
arquivo privado para a senha:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
biaws admin instance update local --password-file /caminho/privado/senha
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Se a política operacional já produz um backup externo verificável, a criação
|
|
69
|
+
local pode ser dispensada explicitamente com `--skip-backup`. O update preserva
|
|
70
|
+
o project name, o `.env`, os volumes e os bind mounts da instância. Ele não
|
|
71
|
+
executa `git pull` nem troca a versão do checkout.
|
|
72
|
+
|
|
73
|
+
A versão implantada só é registrada depois que `up -d --build --wait` termina
|
|
74
|
+
com sucesso. Ela também é exposta pelo `/api/health` e pelos labels dos
|
|
75
|
+
containers de API e UI. A versão nova é lida do `biaws-cli/package.json` da
|
|
76
|
+
raiz selecionada por `--root`/`BIAWS_ROOT`.
|
|
77
|
+
|
|
38
78
|
## Smoke test Docker real
|
|
39
79
|
|
|
40
80
|
Em um checkout descartável com Docker e OpenSSL disponíveis:
|
package/docs/releasing.md
CHANGED
|
@@ -69,9 +69,13 @@ não são aliases circulares. A remoção dos wrappers públicos só pode ocorre
|
|
|
69
69
|
após aviso de depreciação em uma release `0.x`, telemetria ou evidência de
|
|
70
70
|
adoção e um runbook de substituição atualizado.
|
|
71
71
|
|
|
72
|
-
O pacote npm isolado cobre ajuda
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
O pacote npm isolado cobre ajuda, API remota, instalação das skills e
|
|
73
|
+
configuração do MCP publicado. Operações de instância e publicação do catálogo
|
|
74
|
+
inicial dependem do checkout completo; defina `BIAWS_ROOT` para sua raiz quando
|
|
75
|
+
o executável global for chamado fora dela.
|
|
76
|
+
|
|
77
|
+
Publique primeiro a versão de `biaws-mcp` fixada pelo CLI. O CLI nunca deve ser
|
|
78
|
+
publicado apontando para uma versão do MCP que ainda não exista no registry.
|
|
75
79
|
|
|
76
80
|
## Exercício de release
|
|
77
81
|
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../../commands/instance/update.js";
|
package/src/commands/agent.js
CHANGED
|
@@ -9,6 +9,11 @@ import { runSkillsCommand } from "./skills.js";
|
|
|
9
9
|
|
|
10
10
|
const CODEX_BEGIN = "# BEGIN BIAWS MANAGED MCP";
|
|
11
11
|
const CODEX_END = "# END BIAWS MANAGED MCP";
|
|
12
|
+
export const MCP_PACKAGE_NAME = "biaws-mcp";
|
|
13
|
+
export const MCP_PACKAGE_VERSION = "0.1.0";
|
|
14
|
+
export const MCP_PACKAGE_SPEC = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
|
|
15
|
+
const MCP_COMMAND = "npx";
|
|
16
|
+
const MCP_ARGS = ["--yes", MCP_PACKAGE_SPEC];
|
|
12
17
|
|
|
13
18
|
function skillTarget(client, projectDirectory) {
|
|
14
19
|
return path.join(
|
|
@@ -18,8 +23,18 @@ function skillTarget(client, projectDirectory) {
|
|
|
18
23
|
);
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
function
|
|
22
|
-
|
|
26
|
+
function isExpectedClaudeServer(server) {
|
|
27
|
+
if (
|
|
28
|
+
server?.command === MCP_COMMAND &&
|
|
29
|
+
JSON.stringify(server.args || []) === JSON.stringify(MCP_ARGS)
|
|
30
|
+
) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return (
|
|
34
|
+
server?.command === "node" &&
|
|
35
|
+
server.args?.length === 1 &&
|
|
36
|
+
/(?:^|[\\/])biaws-mcp[\\/]src[\\/]index\.js$/u.test(server.args[0])
|
|
37
|
+
);
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
async function readOptional(filePath, fallback = "") {
|
|
@@ -33,7 +48,6 @@ async function readOptional(filePath, fallback = "") {
|
|
|
33
48
|
|
|
34
49
|
async function writeCodexConfiguration(
|
|
35
50
|
projectDirectory,
|
|
36
|
-
entrypoint,
|
|
37
51
|
envFile,
|
|
38
52
|
workspaceId,
|
|
39
53
|
force,
|
|
@@ -59,8 +73,8 @@ async function writeCodexConfiguration(
|
|
|
59
73
|
}
|
|
60
74
|
const managed = `${CODEX_BEGIN}
|
|
61
75
|
[mcp_servers.biaws]
|
|
62
|
-
command =
|
|
63
|
-
args =
|
|
76
|
+
command = ${JSON.stringify(MCP_COMMAND)}
|
|
77
|
+
args = ${JSON.stringify(MCP_ARGS)}
|
|
64
78
|
env = {${envFile ? ` BIAWS_ENV_FILE = ${JSON.stringify(envFile)},` : ""} ISSUE_WORKSPACE_ID = ${JSON.stringify(workspaceId)} }
|
|
65
79
|
${CODEX_END}
|
|
66
80
|
`;
|
|
@@ -71,7 +85,6 @@ ${CODEX_END}
|
|
|
71
85
|
|
|
72
86
|
async function writeClaudeConfiguration(
|
|
73
87
|
projectDirectory,
|
|
74
|
-
entrypoint,
|
|
75
88
|
envFile,
|
|
76
89
|
workspaceId,
|
|
77
90
|
force,
|
|
@@ -85,21 +98,19 @@ async function writeClaudeConfiguration(
|
|
|
85
98
|
throw new Error(`JSON inválido em ${configPath}`);
|
|
86
99
|
}
|
|
87
100
|
config.mcpServers ||= {};
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
}
|
|
101
|
+
if (
|
|
102
|
+
config.mcpServers.biaws &&
|
|
103
|
+
!force &&
|
|
104
|
+
!isExpectedClaudeServer(config.mcpServers.biaws)
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Já existe uma configuração Claude diferente para o servidor biaws em ${configPath}; use --force para substituí-la`,
|
|
108
|
+
);
|
|
98
109
|
}
|
|
99
110
|
config.mcpServers.biaws = {
|
|
100
111
|
type: "stdio",
|
|
101
|
-
command:
|
|
102
|
-
args:
|
|
112
|
+
command: MCP_COMMAND,
|
|
113
|
+
args: MCP_ARGS,
|
|
103
114
|
env: {
|
|
104
115
|
...(envFile ? { BIAWS_ENV_FILE: envFile } : {}),
|
|
105
116
|
ISSUE_WORKSPACE_ID: workspaceId,
|
|
@@ -144,19 +155,16 @@ async function configure(api, client, options, context) {
|
|
|
144
155
|
const projectDirectory = path.resolve(options.project || process.cwd());
|
|
145
156
|
const { workspaceId } = await resolveWorkspaceId(api, context.workspaceId);
|
|
146
157
|
const target = skillTarget(client, projectDirectory);
|
|
147
|
-
const entrypoint = mcpEntrypoint(context.toolDirectory);
|
|
148
158
|
const configPath =
|
|
149
159
|
client === "codex"
|
|
150
160
|
? await writeCodexConfiguration(
|
|
151
161
|
projectDirectory,
|
|
152
|
-
entrypoint,
|
|
153
162
|
context.envFile,
|
|
154
163
|
workspaceId,
|
|
155
164
|
options.force,
|
|
156
165
|
)
|
|
157
166
|
: await writeClaudeConfiguration(
|
|
158
167
|
projectDirectory,
|
|
159
|
-
entrypoint,
|
|
160
168
|
context.envFile,
|
|
161
169
|
workspaceId,
|
|
162
170
|
options.force,
|
|
@@ -173,6 +181,7 @@ async function configure(api, client, options, context) {
|
|
|
173
181
|
configPath,
|
|
174
182
|
skillTarget: target,
|
|
175
183
|
installation,
|
|
184
|
+
mcpPackage: MCP_PACKAGE_SPEC,
|
|
176
185
|
workspaceId,
|
|
177
186
|
};
|
|
178
187
|
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
@@ -197,9 +206,9 @@ async function requestStatus(url, headers = {}) {
|
|
|
197
206
|
}
|
|
198
207
|
}
|
|
199
208
|
|
|
200
|
-
async function mcpStatus(
|
|
209
|
+
async function mcpStatus(envFile, workspaceId) {
|
|
201
210
|
return new Promise((resolve) => {
|
|
202
|
-
const child = spawn(
|
|
211
|
+
const child = spawn(MCP_COMMAND, MCP_ARGS, {
|
|
203
212
|
stdio: ["pipe", "pipe", "pipe"],
|
|
204
213
|
env: {
|
|
205
214
|
...process.env,
|
|
@@ -222,9 +231,9 @@ async function mcpStatus(entrypoint, envFile, workspaceId) {
|
|
|
222
231
|
() =>
|
|
223
232
|
finish({
|
|
224
233
|
ok: false,
|
|
225
|
-
detail: errorOutput.trim() || "MCP não respondeu em
|
|
234
|
+
detail: errorOutput.trim() || "MCP não respondeu em 30 segundos",
|
|
226
235
|
}),
|
|
227
|
-
|
|
236
|
+
30_000,
|
|
228
237
|
);
|
|
229
238
|
child.stderr.on("data", (chunk) => {
|
|
230
239
|
errorOutput += chunk;
|
|
@@ -283,20 +292,15 @@ async function mcpStatus(entrypoint, envFile, workspaceId) {
|
|
|
283
292
|
});
|
|
284
293
|
}
|
|
285
294
|
|
|
286
|
-
function configurationStatus(
|
|
287
|
-
client,
|
|
288
|
-
contents,
|
|
289
|
-
entrypoint,
|
|
290
|
-
envFile,
|
|
291
|
-
workspaceId,
|
|
292
|
-
) {
|
|
295
|
+
function configurationStatus(client, contents, envFile, workspaceId) {
|
|
293
296
|
if (typeof contents !== "string") {
|
|
294
297
|
return { ok: false, detail: "Configuração MCP não encontrada" };
|
|
295
298
|
}
|
|
296
299
|
if (client === "codex") {
|
|
297
300
|
const ok =
|
|
298
301
|
contents.includes("[mcp_servers.biaws]") &&
|
|
299
|
-
contents.includes(JSON.stringify(
|
|
302
|
+
contents.includes(`command = ${JSON.stringify(MCP_COMMAND)}`) &&
|
|
303
|
+
contents.includes(`args = ${JSON.stringify(MCP_ARGS)}`) &&
|
|
300
304
|
(!envFile || contents.includes(JSON.stringify(envFile))) &&
|
|
301
305
|
contents.includes(`ISSUE_WORKSPACE_ID = ${JSON.stringify(workspaceId)}`);
|
|
302
306
|
return {
|
|
@@ -309,8 +313,8 @@ function configurationStatus(
|
|
|
309
313
|
try {
|
|
310
314
|
const server = JSON.parse(contents).mcpServers?.biaws;
|
|
311
315
|
const ok =
|
|
312
|
-
server?.command ===
|
|
313
|
-
JSON.stringify(server.args || []) === JSON.stringify(
|
|
316
|
+
server?.command === MCP_COMMAND &&
|
|
317
|
+
JSON.stringify(server.args || []) === JSON.stringify(MCP_ARGS) &&
|
|
314
318
|
(!envFile || server.env?.BIAWS_ENV_FILE === envFile) &&
|
|
315
319
|
server.env?.ISSUE_WORKSPACE_ID === workspaceId;
|
|
316
320
|
return {
|
|
@@ -336,11 +340,10 @@ async function doctor(api, client, options, context) {
|
|
|
336
340
|
client === "codex"
|
|
337
341
|
? path.join(projectDirectory, ".codex", "config.toml")
|
|
338
342
|
: path.join(projectDirectory, ".mcp.json");
|
|
339
|
-
const entrypoint = mcpEntrypoint(context.toolDirectory);
|
|
340
343
|
const resolved = await resolveWorkspaceId(api, context.workspaceId);
|
|
341
344
|
const [health, mcp, configContents, lock] = await Promise.all([
|
|
342
345
|
requestStatus(`${context.apiUrl}/api/health`),
|
|
343
|
-
mcpStatus(
|
|
346
|
+
mcpStatus(context.envFile, resolved.workspaceId),
|
|
344
347
|
readOptional(configPath, null),
|
|
345
348
|
readLock(target),
|
|
346
349
|
]);
|
|
@@ -354,7 +357,6 @@ async function doctor(api, client, options, context) {
|
|
|
354
357
|
...configurationStatus(
|
|
355
358
|
client,
|
|
356
359
|
configContents,
|
|
357
|
-
entrypoint,
|
|
358
360
|
context.envFile,
|
|
359
361
|
resolved.workspaceId,
|
|
360
362
|
),
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
|
|
3
|
+
import { LocalInstanceCommand } from "../../baseCommands.js";
|
|
4
|
+
import {
|
|
5
|
+
contextFlags,
|
|
6
|
+
contextInput,
|
|
7
|
+
instanceArgument,
|
|
8
|
+
} from "../../instance/command.js";
|
|
9
|
+
import {
|
|
10
|
+
backupArguments,
|
|
11
|
+
executeInstanceScript,
|
|
12
|
+
getInstance,
|
|
13
|
+
getInstanceUpdateStatus,
|
|
14
|
+
updateInstance,
|
|
15
|
+
validateInstanceUpdate,
|
|
16
|
+
withPasswordFile,
|
|
17
|
+
} from "../../instance/service.js";
|
|
18
|
+
|
|
19
|
+
function versionCheckResult(instance, versionStatus, json) {
|
|
20
|
+
if (json) return versionStatus;
|
|
21
|
+
return [
|
|
22
|
+
`Instância: ${instance.name}`,
|
|
23
|
+
`Versão atual: ${versionStatus.currentVersion}`,
|
|
24
|
+
`Nova versão: ${versionStatus.newVersion}`,
|
|
25
|
+
`Atualização necessária: ${versionStatus.updateRequired ? "sim" : "não"}`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function unchangedResult(instance, versionStatus, json) {
|
|
30
|
+
return json
|
|
31
|
+
? { ...versionStatus, updated: false, backupCreated: false }
|
|
32
|
+
: `Instância ${instance.name} já está na versão ${versionStatus.newVersion}.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function createUpdateBackup(command, instance, context, flags) {
|
|
36
|
+
if (flags["skip-backup"]) return "";
|
|
37
|
+
|
|
38
|
+
let password;
|
|
39
|
+
if (!flags["password-file"]) {
|
|
40
|
+
if (!context.isInteractive) {
|
|
41
|
+
command.error(
|
|
42
|
+
"Atualização exige --password-file ou --skip-backup em modo não interativo.",
|
|
43
|
+
{ code: "BACKUP_PASSWORD_REQUIRED", exit: 2 },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
password = await command.adapters.prompts.ask({
|
|
47
|
+
name: "password",
|
|
48
|
+
type: "password",
|
|
49
|
+
message: "Senha do backup anterior à atualização",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return withPasswordFile(
|
|
54
|
+
command.adapters.filesystem,
|
|
55
|
+
password,
|
|
56
|
+
flags["password-file"],
|
|
57
|
+
async (passwordFile) =>
|
|
58
|
+
executeInstanceScript(
|
|
59
|
+
backupArguments(instance, context, {
|
|
60
|
+
output: flags["backup-output"],
|
|
61
|
+
passwordFile,
|
|
62
|
+
}),
|
|
63
|
+
context,
|
|
64
|
+
command.adapters.processRunner,
|
|
65
|
+
{ secrets: password ? [password] : [], silent: flags.json },
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updatedResult(instance, versionStatus, flags, backupOutput) {
|
|
71
|
+
if (!flags.json) {
|
|
72
|
+
return `Instância ${instance.name} atualizada de ${versionStatus.currentVersion} para ${versionStatus.newVersion}. UI: ${instance.publicUrl}`;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
instance: instance.name,
|
|
76
|
+
operation: "update",
|
|
77
|
+
currentVersion: versionStatus.currentVersion,
|
|
78
|
+
newVersion: versionStatus.newVersion,
|
|
79
|
+
updateRequired: versionStatus.updateRequired,
|
|
80
|
+
updated: true,
|
|
81
|
+
backupCreated: !flags["skip-backup"],
|
|
82
|
+
backupOutput,
|
|
83
|
+
ui: instance.publicUrl,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default class InstanceUpdate extends LocalInstanceCommand {
|
|
88
|
+
static description =
|
|
89
|
+
"reconstrói e atualiza os serviços preservando os dados da instância";
|
|
90
|
+
static args = { instance: instanceArgument };
|
|
91
|
+
static flags = {
|
|
92
|
+
...contextFlags,
|
|
93
|
+
"backup-output": Flags.string({
|
|
94
|
+
description: "arquivo de destino do backup anterior à atualização",
|
|
95
|
+
}),
|
|
96
|
+
"password-file": Flags.string({
|
|
97
|
+
description: "arquivo privado contendo a senha do backup",
|
|
98
|
+
}),
|
|
99
|
+
"skip-backup": Flags.boolean({
|
|
100
|
+
description: "atualiza sem criar o backup de segurança",
|
|
101
|
+
exclusive: ["backup-output", "password-file"],
|
|
102
|
+
}),
|
|
103
|
+
check: Flags.boolean({
|
|
104
|
+
description: "compara a versão instalada com a versão da release",
|
|
105
|
+
exclusive: ["backup-output", "password-file", "skip-backup", "force"],
|
|
106
|
+
}),
|
|
107
|
+
force: Flags.boolean({
|
|
108
|
+
description: "reconstrói mesmo quando a instância já está na versão nova",
|
|
109
|
+
exclusive: ["check"],
|
|
110
|
+
}),
|
|
111
|
+
json: Flags.boolean({ description: "emite resultado JSON" }),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
async run() {
|
|
115
|
+
const { args, flags } = await this.parse(InstanceUpdate);
|
|
116
|
+
const context = await this.localContext(contextInput(flags, args.instance));
|
|
117
|
+
const instance = await getInstance(
|
|
118
|
+
context,
|
|
119
|
+
this.adapters.filesystem,
|
|
120
|
+
args.instance,
|
|
121
|
+
);
|
|
122
|
+
const versionStatus = await getInstanceUpdateStatus(
|
|
123
|
+
instance,
|
|
124
|
+
context,
|
|
125
|
+
this.adapters.filesystem,
|
|
126
|
+
);
|
|
127
|
+
if (flags.check) {
|
|
128
|
+
this.output({ json: flags.json }).result(
|
|
129
|
+
versionCheckResult(instance, versionStatus, flags.json),
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!versionStatus.updateRequired && !flags.force) {
|
|
134
|
+
this.output({ json: flags.json }).result(
|
|
135
|
+
unchangedResult(instance, versionStatus, flags.json),
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await validateInstanceUpdate(
|
|
141
|
+
instance,
|
|
142
|
+
context,
|
|
143
|
+
this.adapters.processRunner,
|
|
144
|
+
);
|
|
145
|
+
const backupOutput = await createUpdateBackup(
|
|
146
|
+
this,
|
|
147
|
+
instance,
|
|
148
|
+
context,
|
|
149
|
+
flags,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await updateInstance(
|
|
153
|
+
instance,
|
|
154
|
+
context,
|
|
155
|
+
this.adapters.filesystem,
|
|
156
|
+
this.adapters.processRunner,
|
|
157
|
+
versionStatus.newVersion,
|
|
158
|
+
this.adapters.environment,
|
|
159
|
+
);
|
|
160
|
+
this.output({ json: flags.json }).result(
|
|
161
|
+
updatedResult(instance, versionStatus, flags, backupOutput),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/commands/instance.js
CHANGED
package/src/instance/service.js
CHANGED
|
@@ -18,6 +18,8 @@ const STORAGE_FIELDS = Object.freeze({
|
|
|
18
18
|
secretPath: "BIAWS_SECRET_FILES_PATH",
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
const UNKNOWN_VERSION = "unknown";
|
|
22
|
+
|
|
21
23
|
function usageError(message, code, details) {
|
|
22
24
|
throw new CliError(message, { code, details, exitCode: 2 });
|
|
23
25
|
}
|
|
@@ -176,6 +178,50 @@ export async function getInstance(context, filesystem, name) {
|
|
|
176
178
|
return instance;
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
function normalizeVersion(value, label) {
|
|
182
|
+
const version = String(value || "").trim();
|
|
183
|
+
if (
|
|
184
|
+
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(version)
|
|
185
|
+
) {
|
|
186
|
+
throw new CliError(`${label} inválida: ${version || "ausente"}.`, {
|
|
187
|
+
code: "INVALID_RELEASE_VERSION",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return version;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function getInstanceUpdateStatus(instance, context, filesystem) {
|
|
194
|
+
const manifestPath = path.join(
|
|
195
|
+
context.repositoryRoot,
|
|
196
|
+
"biaws-cli",
|
|
197
|
+
"package.json",
|
|
198
|
+
);
|
|
199
|
+
let manifest;
|
|
200
|
+
try {
|
|
201
|
+
manifest = JSON.parse(await filesystem.readFile(manifestPath, "utf8"));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new CliError(
|
|
204
|
+
`Não foi possível ler a versão da release em ${manifestPath}.`,
|
|
205
|
+
{
|
|
206
|
+
code: "RELEASE_VERSION_READ_FAILED",
|
|
207
|
+
cause: error,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const newVersion = normalizeVersion(manifest.version, "Versão da release");
|
|
212
|
+
const installed = String(instance.env.BIAWS_VERSION || "").trim();
|
|
213
|
+
const currentVersion =
|
|
214
|
+
installed && installed !== UNKNOWN_VERSION
|
|
215
|
+
? normalizeVersion(installed, "Versão instalada")
|
|
216
|
+
: UNKNOWN_VERSION;
|
|
217
|
+
return Object.freeze({
|
|
218
|
+
instance: instance.name,
|
|
219
|
+
currentVersion,
|
|
220
|
+
newVersion,
|
|
221
|
+
updateRequired: currentVersion !== newVersion,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
179
225
|
function resolveStorage(values, existing) {
|
|
180
226
|
if (values.storage === "volumes") {
|
|
181
227
|
if (Object.keys(STORAGE_FIELDS).some((field) => values[field])) {
|
|
@@ -396,6 +442,8 @@ export function composeArguments(instance, context, operation) {
|
|
|
396
442
|
`biaws-${instance.name}`,
|
|
397
443
|
];
|
|
398
444
|
if (operation === "start") return [...base, "up", "-d", "--wait"];
|
|
445
|
+
if (operation === "update") return [...base, "up", "-d", "--build", "--wait"];
|
|
446
|
+
if (operation === "validate") return [...base, "config", "--quiet"];
|
|
399
447
|
if (operation === "stop") return [...base, "stop"];
|
|
400
448
|
return [...base, "ps", "--format", "json"];
|
|
401
449
|
}
|
|
@@ -405,15 +453,49 @@ export async function operateInstance(
|
|
|
405
453
|
context,
|
|
406
454
|
processRunner,
|
|
407
455
|
operation,
|
|
456
|
+
options = {},
|
|
408
457
|
) {
|
|
409
458
|
const result = await processRunner.run(
|
|
410
459
|
"docker",
|
|
411
460
|
composeArguments(instance, context, operation),
|
|
412
|
-
{
|
|
461
|
+
{
|
|
462
|
+
cwd: context.repositoryRoot,
|
|
463
|
+
env: options.environment,
|
|
464
|
+
silent: true,
|
|
465
|
+
},
|
|
413
466
|
);
|
|
414
467
|
return { instance: instance.name, operation, output: result.stdout.trim() };
|
|
415
468
|
}
|
|
416
469
|
|
|
470
|
+
export async function validateInstanceUpdate(instance, context, processRunner) {
|
|
471
|
+
return operateInstance(instance, context, processRunner, "validate");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function updateInstance(
|
|
475
|
+
instance,
|
|
476
|
+
context,
|
|
477
|
+
filesystem,
|
|
478
|
+
processRunner,
|
|
479
|
+
version,
|
|
480
|
+
environment = {},
|
|
481
|
+
) {
|
|
482
|
+
const normalizedVersion = normalizeVersion(version, "Nova versão");
|
|
483
|
+
const result = await operateInstance(
|
|
484
|
+
instance,
|
|
485
|
+
context,
|
|
486
|
+
processRunner,
|
|
487
|
+
"update",
|
|
488
|
+
{ environment: { ...environment, BIAWS_VERSION: normalizedVersion } },
|
|
489
|
+
);
|
|
490
|
+
const envContents = await filesystem.readFile(instance.envFile, "utf8");
|
|
491
|
+
const versionLine = `BIAWS_VERSION=${normalizedVersion}`;
|
|
492
|
+
const nextEnv = /^BIAWS_VERSION=.*$/mu.test(envContents)
|
|
493
|
+
? envContents.replace(/^BIAWS_VERSION=.*$/gmu, versionLine)
|
|
494
|
+
: `${envContents.replace(/\s*$/u, "")}\n${versionLine}\n`;
|
|
495
|
+
await filesystem.writeFile(instance.envFile, nextEnv, { mode: 0o600 });
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
417
499
|
export function validateArchivePath(value) {
|
|
418
500
|
const archive = path.resolve(String(value || ""));
|
|
419
501
|
if (!value || archive === path.parse(archive).root) {
|