biaws 0.2.1 → 0.3.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/README.md CHANGED
@@ -122,6 +122,7 @@ biaws workspace api PATCH /recurso/ID --body '{"status":"Ativo"}'
122
122
  ```bash
123
123
  biaws workspace agent configure codex
124
124
  biaws workspace agent configure claude
125
+ biaws workspace agent configure codex --no-interactive --env-file /caminho/instancia.env --workspace workspace-id
125
126
  biaws workspace agent doctor codex
126
127
  biaws workspace skills list
127
128
  biaws workspace skills install SKILL_ID
@@ -131,7 +132,9 @@ biaws workspace skills status
131
132
 
132
133
  O Codex usa `.codex/config.toml` e `.agents/skills`. O Claude Code usa
133
134
  `.mcp.json` e `.claude/skills`. Configurações de terceiros são preservadas e
134
- nenhuma credencial é gravada no projeto.
135
+ nenhuma credencial é gravada no projeto. O MCP é configurado como
136
+ `npx --yes biaws-mcp@<versão-fixada>`, sem exigir um checkout local da
137
+ plataforma.
135
138
 
136
139
  ## Monitoramento
137
140
 
@@ -171,6 +174,7 @@ biaws admin instance list
171
174
  biaws admin instance status local
172
175
  biaws admin instance start local
173
176
  biaws admin instance stop local
177
+ biaws admin instance update local
174
178
  biaws admin instance backup local
175
179
  ```
176
180
 
@@ -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
@@ -61,9 +61,8 @@ flags > variáveis de ambiente > configuração da pasta > perfil global > defau
61
61
  ```
62
62
 
63
63
  As variáveis canônicas são `BIAWS_API_URL`, `BIAWS_API_KEY` e
64
- `BIAWS_WORKSPACE_ID`. Os nomes `ISSUE_*` permanecem aceitos como variáveis de
65
- ambiente internas durante a transição do backend, mas não representam comandos
66
- antigos do CLI.
64
+ `BIAWS_WORKSPACE_ID`. Variáveis com o prefixo temporário anterior não são
65
+ aceitas como aliases.
67
66
 
68
67
  ## Convenções
69
68
 
@@ -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 e API remota. Instância, MCP e catálogo local
73
- dependem do checkout completo; defina `BIAWS_ROOT` para sua raiz quando o
74
- executável global for chamado fora dela.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biaws",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI do Bondia Workspaces para administração, configuração e operação de workspaces",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1 @@
1
+ export { default } from "../../../commands/instance/update.js";
@@ -4,6 +4,7 @@ import { AuthenticatedApiCommand, ProjectCommand } from "./baseCommands.js";
4
4
  import {
5
5
  configureContextFlags,
6
6
  configureContextInput,
7
+ interactiveConfigureInput,
7
8
  legacyAgentContext,
8
9
  } from "./configure/command.js";
9
10
  import { runAgentCommand } from "./commands/agent.js";
@@ -85,12 +86,21 @@ export function createAgentCommand(action) {
85
86
 
86
87
  async run() {
87
88
  const { args, flags } = await this.parse(this.constructor);
88
- const context = await this.projectContext(configureContextInput(flags));
89
+ const input =
90
+ action === "configure"
91
+ ? await interactiveConfigureInput(this, flags)
92
+ : configureContextInput(flags);
93
+ const context = await this.projectContext(input);
89
94
  await runAgentCommand(
90
95
  context.api,
91
96
  action,
92
97
  [args.client],
93
- { ...flags, project: context.projectDirectory },
98
+ {
99
+ ...flags,
100
+ interactive: input.interactive,
101
+ project: context.projectDirectory,
102
+ prompts: this.adapters.prompts,
103
+ },
94
104
  legacyAgentContext(context),
95
105
  );
96
106
  }
@@ -4,11 +4,17 @@ import path from "node:path";
4
4
 
5
5
  import { BaseCommand, TOOL_DIRECTORY } from "../baseCommands.js";
6
6
  import { showTopic } from "../commandFactories.js";
7
+ import { CliError } from "../core/errors.js";
7
8
  import { readLock } from "../localSkills.js";
8
9
  import { runSkillsCommand } from "./skills.js";
9
10
 
10
11
  const CODEX_BEGIN = "# BEGIN BIAWS MANAGED MCP";
11
12
  const CODEX_END = "# END BIAWS MANAGED MCP";
13
+ export const MCP_PACKAGE_NAME = "biaws-mcp";
14
+ export const MCP_PACKAGE_VERSION = "0.2.0";
15
+ export const MCP_PACKAGE_SPEC = `${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION}`;
16
+ const MCP_COMMAND = "npx";
17
+ const MCP_ARGS = ["--yes", MCP_PACKAGE_SPEC];
12
18
 
13
19
  function skillTarget(client, projectDirectory) {
14
20
  return path.join(
@@ -18,8 +24,18 @@ function skillTarget(client, projectDirectory) {
18
24
  );
19
25
  }
20
26
 
21
- function mcpEntrypoint(toolDirectory) {
22
- return path.resolve(toolDirectory, "..", "biaws-mcp", "src", "index.js");
27
+ function isExpectedClaudeServer(server) {
28
+ if (
29
+ server?.command === MCP_COMMAND &&
30
+ JSON.stringify(server.args || []) === JSON.stringify(MCP_ARGS)
31
+ ) {
32
+ return true;
33
+ }
34
+ return (
35
+ server?.command === "node" &&
36
+ server.args?.length === 1 &&
37
+ /(?:^|[\\/])biaws-mcp[\\/]src[\\/]index\.js$/u.test(server.args[0])
38
+ );
23
39
  }
24
40
 
25
41
  async function readOptional(filePath, fallback = "") {
@@ -33,7 +49,6 @@ async function readOptional(filePath, fallback = "") {
33
49
 
34
50
  async function writeCodexConfiguration(
35
51
  projectDirectory,
36
- entrypoint,
37
52
  envFile,
38
53
  workspaceId,
39
54
  force,
@@ -59,9 +74,9 @@ async function writeCodexConfiguration(
59
74
  }
60
75
  const managed = `${CODEX_BEGIN}
61
76
  [mcp_servers.biaws]
62
- command = "node"
63
- args = [${JSON.stringify(entrypoint)}]
64
- env = {${envFile ? ` BIAWS_ENV_FILE = ${JSON.stringify(envFile)},` : ""} ISSUE_WORKSPACE_ID = ${JSON.stringify(workspaceId)} }
77
+ command = ${JSON.stringify(MCP_COMMAND)}
78
+ args = ${JSON.stringify(MCP_ARGS)}
79
+ env = {${envFile ? ` BIAWS_ENV_FILE = ${JSON.stringify(envFile)},` : ""} BIAWS_WORKSPACE_ID = ${JSON.stringify(workspaceId)} }
65
80
  ${CODEX_END}
66
81
  `;
67
82
  const output = current ? `${current}\n\n${managed}` : managed;
@@ -71,7 +86,6 @@ ${CODEX_END}
71
86
 
72
87
  async function writeClaudeConfiguration(
73
88
  projectDirectory,
74
- entrypoint,
75
89
  envFile,
76
90
  workspaceId,
77
91
  force,
@@ -85,31 +99,34 @@ async function writeClaudeConfiguration(
85
99
  throw new Error(`JSON inválido em ${configPath}`);
86
100
  }
87
101
  config.mcpServers ||= {};
88
- if (config.mcpServers.biaws && !force) {
89
- const current = config.mcpServers.biaws;
90
- if (
91
- current.command !== "node" ||
92
- JSON.stringify(current.args || []) !== JSON.stringify([entrypoint])
93
- ) {
94
- throw new Error(
95
- `Já existe uma configuração Claude diferente para o servidor biaws em ${configPath}; use --force para substituí-la`,
96
- );
97
- }
102
+ if (
103
+ config.mcpServers.biaws &&
104
+ !force &&
105
+ !isExpectedClaudeServer(config.mcpServers.biaws)
106
+ ) {
107
+ throw new Error(
108
+ `Já existe uma configuração Claude diferente para o servidor biaws em ${configPath}; use --force para substituí-la`,
109
+ );
98
110
  }
99
111
  config.mcpServers.biaws = {
100
112
  type: "stdio",
101
- command: "node",
102
- args: [entrypoint],
113
+ command: MCP_COMMAND,
114
+ args: MCP_ARGS,
103
115
  env: {
104
116
  ...(envFile ? { BIAWS_ENV_FILE: envFile } : {}),
105
- ISSUE_WORKSPACE_ID: workspaceId,
117
+ BIAWS_WORKSPACE_ID: workspaceId,
106
118
  },
107
119
  };
108
120
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
109
121
  return configPath;
110
122
  }
111
123
 
112
- async function resolveWorkspaceId(api, requestedWorkspaceId = "") {
124
+ async function resolveWorkspaceId(
125
+ api,
126
+ requestedWorkspaceId = "",
127
+ interactive = false,
128
+ prompts,
129
+ ) {
113
130
  let identity;
114
131
  try {
115
132
  identity = await api.identity();
@@ -121,7 +138,22 @@ async function resolveWorkspaceId(api, requestedWorkspaceId = "") {
121
138
  }
122
139
  throw error;
123
140
  }
124
- const workspaceId = requestedWorkspaceId || identity?.actor?.workspaceId;
141
+ const workspaces = identity?.actor?.workspaces || [];
142
+ let workspaceId = requestedWorkspaceId || identity?.actor?.workspaceId;
143
+ if (interactive && workspaces.length > 0) {
144
+ workspaceId = await prompts.ask({
145
+ name: "workspaceId",
146
+ type: "select",
147
+ message: "Workspace do projeto",
148
+ choices: workspaces.map((workspace) => ({
149
+ name: workspace.name
150
+ ? `${workspace.name} (${workspace.id})`
151
+ : workspace.id,
152
+ value: workspace.id,
153
+ })),
154
+ ...(workspaceId ? { default: workspaceId } : {}),
155
+ });
156
+ }
125
157
  if (!workspaceId) {
126
158
  if ((identity?.actor?.workspaces?.length || 0) > 1) {
127
159
  throw new Error(
@@ -142,21 +174,40 @@ async function configure(api, client, options, context) {
142
174
  );
143
175
  }
144
176
  const projectDirectory = path.resolve(options.project || process.cwd());
145
- const { workspaceId } = await resolveWorkspaceId(api, context.workspaceId);
177
+ const { workspaceId } = await resolveWorkspaceId(
178
+ api,
179
+ context.workspaceId,
180
+ options.interactive,
181
+ options.prompts,
182
+ );
183
+ if (options.interactive) {
184
+ const confirmed = await options.prompts.ask({
185
+ name: "confirmConfiguration",
186
+ type: "confirm",
187
+ message: `Configurar ${client} em ${projectDirectory} para o workspace ${workspaceId}?`,
188
+ default: true,
189
+ });
190
+ if (!confirmed) {
191
+ throw new CliError(
192
+ "Configuração cancelada antes de qualquer alteração.",
193
+ {
194
+ code: "PROMPT_CANCELLED",
195
+ exitCode: 130,
196
+ },
197
+ );
198
+ }
199
+ }
146
200
  const target = skillTarget(client, projectDirectory);
147
- const entrypoint = mcpEntrypoint(context.toolDirectory);
148
201
  const configPath =
149
202
  client === "codex"
150
203
  ? await writeCodexConfiguration(
151
204
  projectDirectory,
152
- entrypoint,
153
205
  context.envFile,
154
206
  workspaceId,
155
207
  options.force,
156
208
  )
157
209
  : await writeClaudeConfiguration(
158
210
  projectDirectory,
159
- entrypoint,
160
211
  context.envFile,
161
212
  workspaceId,
162
213
  options.force,
@@ -173,6 +224,7 @@ async function configure(api, client, options, context) {
173
224
  configPath,
174
225
  skillTarget: target,
175
226
  installation,
227
+ mcpPackage: MCP_PACKAGE_SPEC,
176
228
  workspaceId,
177
229
  };
178
230
  if (options.json) console.log(JSON.stringify(result, null, 2));
@@ -197,14 +249,14 @@ async function requestStatus(url, headers = {}) {
197
249
  }
198
250
  }
199
251
 
200
- async function mcpStatus(entrypoint, envFile, workspaceId) {
252
+ async function mcpStatus(envFile, workspaceId) {
201
253
  return new Promise((resolve) => {
202
- const child = spawn(process.execPath, [entrypoint], {
254
+ const child = spawn(MCP_COMMAND, MCP_ARGS, {
203
255
  stdio: ["pipe", "pipe", "pipe"],
204
256
  env: {
205
257
  ...process.env,
206
258
  ...(envFile ? { BIAWS_ENV_FILE: envFile } : {}),
207
- ISSUE_WORKSPACE_ID: workspaceId,
259
+ BIAWS_WORKSPACE_ID: workspaceId,
208
260
  },
209
261
  });
210
262
  let output = "";
@@ -222,9 +274,9 @@ async function mcpStatus(entrypoint, envFile, workspaceId) {
222
274
  () =>
223
275
  finish({
224
276
  ok: false,
225
- detail: errorOutput.trim() || "MCP não respondeu em 5 segundos",
277
+ detail: errorOutput.trim() || "MCP não respondeu em 30 segundos",
226
278
  }),
227
- 5_000,
279
+ 30_000,
228
280
  );
229
281
  child.stderr.on("data", (chunk) => {
230
282
  errorOutput += chunk;
@@ -283,22 +335,17 @@ async function mcpStatus(entrypoint, envFile, workspaceId) {
283
335
  });
284
336
  }
285
337
 
286
- function configurationStatus(
287
- client,
288
- contents,
289
- entrypoint,
290
- envFile,
291
- workspaceId,
292
- ) {
338
+ function configurationStatus(client, contents, envFile, workspaceId) {
293
339
  if (typeof contents !== "string") {
294
340
  return { ok: false, detail: "Configuração MCP não encontrada" };
295
341
  }
296
342
  if (client === "codex") {
297
343
  const ok =
298
344
  contents.includes("[mcp_servers.biaws]") &&
299
- contents.includes(JSON.stringify(entrypoint)) &&
345
+ contents.includes(`command = ${JSON.stringify(MCP_COMMAND)}`) &&
346
+ contents.includes(`args = ${JSON.stringify(MCP_ARGS)}`) &&
300
347
  (!envFile || contents.includes(JSON.stringify(envFile))) &&
301
- contents.includes(`ISSUE_WORKSPACE_ID = ${JSON.stringify(workspaceId)}`);
348
+ contents.includes(`BIAWS_WORKSPACE_ID = ${JSON.stringify(workspaceId)}`);
302
349
  return {
303
350
  ok,
304
351
  detail: ok
@@ -309,10 +356,10 @@ function configurationStatus(
309
356
  try {
310
357
  const server = JSON.parse(contents).mcpServers?.biaws;
311
358
  const ok =
312
- server?.command === "node" &&
313
- JSON.stringify(server.args || []) === JSON.stringify([entrypoint]) &&
359
+ server?.command === MCP_COMMAND &&
360
+ JSON.stringify(server.args || []) === JSON.stringify(MCP_ARGS) &&
314
361
  (!envFile || server.env?.BIAWS_ENV_FILE === envFile) &&
315
- server.env?.ISSUE_WORKSPACE_ID === workspaceId;
362
+ server.env?.BIAWS_WORKSPACE_ID === workspaceId;
316
363
  return {
317
364
  ok,
318
365
  detail: ok
@@ -336,11 +383,10 @@ async function doctor(api, client, options, context) {
336
383
  client === "codex"
337
384
  ? path.join(projectDirectory, ".codex", "config.toml")
338
385
  : path.join(projectDirectory, ".mcp.json");
339
- const entrypoint = mcpEntrypoint(context.toolDirectory);
340
386
  const resolved = await resolveWorkspaceId(api, context.workspaceId);
341
387
  const [health, mcp, configContents, lock] = await Promise.all([
342
388
  requestStatus(`${context.apiUrl}/api/health`),
343
- mcpStatus(entrypoint, context.envFile, resolved.workspaceId),
389
+ mcpStatus(context.envFile, resolved.workspaceId),
344
390
  readOptional(configPath, null),
345
391
  readLock(target),
346
392
  ]);
@@ -354,7 +400,6 @@ async function doctor(api, client, options, context) {
354
400
  ...configurationStatus(
355
401
  client,
356
402
  configContents,
357
- entrypoint,
358
403
  context.envFile,
359
404
  resolved.workspaceId,
360
405
  ),
@@ -1,7 +1,7 @@
1
1
  import { ProjectCommand } from "../../baseCommands.js";
2
2
  import {
3
3
  configureContextFlags,
4
- configureContextInput,
4
+ interactiveConfigureInput,
5
5
  legacyAgentContext,
6
6
  } from "../../configure/command.js";
7
7
  import { runAgentCommand } from "../agent.js";
@@ -13,12 +13,18 @@ export default class ConfigureClaude extends ProjectCommand {
13
13
 
14
14
  async run() {
15
15
  const { flags } = await this.parse(ConfigureClaude);
16
- const context = await this.projectContext(configureContextInput(flags));
16
+ const input = await interactiveConfigureInput(this, flags);
17
+ const context = await this.projectContext(input);
17
18
  await runAgentCommand(
18
19
  context.api,
19
20
  "configure",
20
21
  ["claude"],
21
- { ...flags, project: context.projectDirectory },
22
+ {
23
+ ...flags,
24
+ interactive: input.interactive,
25
+ project: context.projectDirectory,
26
+ prompts: this.adapters.prompts,
27
+ },
22
28
  legacyAgentContext(context),
23
29
  );
24
30
  }
@@ -1,7 +1,7 @@
1
1
  import { ProjectCommand } from "../../baseCommands.js";
2
2
  import {
3
3
  configureContextFlags,
4
- configureContextInput,
4
+ interactiveConfigureInput,
5
5
  legacyAgentContext,
6
6
  } from "../../configure/command.js";
7
7
  import { runAgentCommand } from "../agent.js";
@@ -13,12 +13,18 @@ export default class ConfigureCodex extends ProjectCommand {
13
13
 
14
14
  async run() {
15
15
  const { flags } = await this.parse(ConfigureCodex);
16
- const context = await this.projectContext(configureContextInput(flags));
16
+ const input = await interactiveConfigureInput(this, flags);
17
+ const context = await this.projectContext(input);
17
18
  await runAgentCommand(
18
19
  context.api,
19
20
  "configure",
20
21
  ["codex"],
21
- { ...flags, project: context.projectDirectory },
22
+ {
23
+ ...flags,
24
+ interactive: input.interactive,
25
+ project: context.projectDirectory,
26
+ prompts: this.adapters.prompts,
27
+ },
22
28
  legacyAgentContext(context),
23
29
  );
24
30
  }
@@ -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
+ }
@@ -8,6 +8,7 @@ export default class Instance extends LocalInstanceCommand {
8
8
  static examples = [
9
9
  "<%= config.bin %> instance setup",
10
10
  "<%= config.bin %> instance status",
11
+ "<%= config.bin %> instance update",
11
12
  "<%= config.bin %> instance backup",
12
13
  ];
13
14
 
@@ -52,7 +52,7 @@ function credentialsWithKey(configuration, name, apiKey) {
52
52
  async function requiredApiKey(command) {
53
53
  const fromEnvironment = String(
54
54
  command.adapters.environment.BIAWS_API_KEY ||
55
- command.adapters.environment.ISSUE_API_KEY ||
55
+ command.adapters.environment.BIAWS_API_KEY ||
56
56
  "",
57
57
  ).trim();
58
58
  if (fromEnvironment) return fromEnvironment;
@@ -2,6 +2,8 @@ import path from "node:path";
2
2
 
3
3
  import { Args, Flags } from "@oclif/core";
4
4
 
5
+ import { CliError } from "../core/errors.js";
6
+
5
7
  export const configureContextFlags = Object.freeze({
6
8
  project: Flags.string({
7
9
  char: "p",
@@ -16,6 +18,12 @@ export const configureContextFlags = Object.freeze({
16
18
  "env-file": Flags.string({
17
19
  description: "arquivo privado com URL e chave de API",
18
20
  }),
21
+ interactive: Flags.boolean({
22
+ char: "i",
23
+ allowNo: true,
24
+ description:
25
+ "usa o modo assistente (padrão em terminais; use --no-interactive para desativar)",
26
+ }),
19
27
  force: Flags.boolean({
20
28
  char: "f",
21
29
  description: "substitui somente a configuração BIAWS conflitante",
@@ -23,6 +31,46 @@ export const configureContextFlags = Object.freeze({
23
31
  json: Flags.boolean({ description: "emite somente JSON em stdout" }),
24
32
  });
25
33
 
34
+ export async function interactiveConfigureInput(command, flags) {
35
+ const interactive =
36
+ flags.interactive === true ||
37
+ (flags.interactive !== false && command.adapters.terminal.isInteractive);
38
+ if (!interactive) {
39
+ return { ...configureContextInput(flags), interactive: false };
40
+ }
41
+ if (!command.adapters.prompts.isInteractive) {
42
+ throw new CliError("O modo assistente exige um terminal interativo.", {
43
+ code: "INTERACTIVE_INPUT_UNAVAILABLE",
44
+ exitCode: 2,
45
+ });
46
+ }
47
+ const project = String(
48
+ await command.adapters.prompts.ask({
49
+ name: "project",
50
+ message: "Diretório do projeto",
51
+ default: flags.project || command.adapters.cwd(),
52
+ }),
53
+ ).trim();
54
+ const envFile = String(
55
+ await command.adapters.prompts.ask({
56
+ name: "envFile",
57
+ message: "Arquivo privado da instância (BIAWS_ENV_FILE)",
58
+ default:
59
+ flags["env-file"] || command.adapters.environment.BIAWS_ENV_FILE || "",
60
+ }),
61
+ ).trim();
62
+ if (!envFile) {
63
+ throw new CliError("Informe o arquivo privado da instância.", {
64
+ code: "ENV_FILE_REQUIRED",
65
+ exitCode: 2,
66
+ });
67
+ }
68
+ return {
69
+ ...configureContextInput({ ...flags, project, "env-file": envFile }),
70
+ interactive: true,
71
+ };
72
+ }
73
+
26
74
  export const clientArgument = Args.string({
27
75
  description: "cliente a diagnosticar",
28
76
  options: ["codex", "claude"],
@@ -126,32 +126,31 @@ export async function resolveCommandContext(options) {
126
126
  const configuredApiUrl =
127
127
  input.apiUrl ||
128
128
  explicitFileEnv.BIAWS_API_URL ||
129
- explicitFileEnv.ISSUE_API_URL ||
129
+ explicitFileEnv.BIAWS_API_URL ||
130
+ environment.BIAWS_API_URL ||
130
131
  environment.BIAWS_API_URL ||
131
- environment.ISSUE_API_URL ||
132
- environment.ISSUE_API_BASE_URL ||
132
+ environment.BIAWS_API_BASE_URL ||
133
133
  workspaceConfiguration.apiUrl ||
134
134
  profile.config.apiUrl ||
135
- effectiveEnv.ISSUE_API_URL ||
136
- effectiveEnv.ISSUE_API_BASE_URL ||
135
+ effectiveEnv.BIAWS_API_URL ||
136
+ effectiveEnv.BIAWS_API_BASE_URL ||
137
137
  DEFAULT_API_URL;
138
138
  const configuredApiKey = String(
139
139
  input.apiKey ||
140
140
  explicitFileEnv.BIAWS_API_KEY ||
141
- explicitFileEnv.ISSUE_API_KEY ||
141
+ explicitFileEnv.BIAWS_API_KEY ||
142
+ environment.BIAWS_API_KEY ||
142
143
  environment.BIAWS_API_KEY ||
143
- environment.ISSUE_API_KEY ||
144
144
  profile.credentials.apiKey ||
145
- effectiveEnv.ISSUE_API_KEY ||
145
+ effectiveEnv.BIAWS_API_KEY ||
146
146
  "",
147
147
  );
148
148
  const configuredWorkspaceId = String(
149
149
  input.workspaceId ||
150
150
  input.workspace ||
151
151
  environment.BIAWS_WORKSPACE_ID ||
152
- environment.ISSUE_WORKSPACE_ID ||
153
152
  workspaceConfiguration.workspaceId ||
154
- effectiveEnv.ISSUE_WORKSPACE_ID ||
153
+ effectiveEnv.BIAWS_WORKSPACE_ID ||
155
154
  "",
156
155
  ).trim();
157
156
 
@@ -6,8 +6,8 @@ import { parseEnv } from "../core/context.js";
6
6
 
7
7
  const PORT_FIELDS = Object.freeze({
8
8
  mongoPort: "MONGO_PORT",
9
- apiPort: "ISSUE_API_PORT",
10
- uiPort: "ISSUE_UI_PORT",
9
+ apiPort: "BIAWS_API_PORT",
10
+ uiPort: "BIAWS_UI_PORT",
11
11
  });
12
12
 
13
13
  const STORAGE_FIELDS = Object.freeze({
@@ -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
  }
@@ -141,11 +143,11 @@ export async function listInstances(context, filesystem) {
141
143
  directory,
142
144
  envFile: path.join(directory, ".env"),
143
145
  mongoPort: Number(env.MONGO_PORT || 27_017),
144
- apiPort: Number(env.ISSUE_API_PORT || 3_100),
145
- uiPort: Number(env.ISSUE_UI_PORT || 4_400),
146
+ apiPort: Number(env.BIAWS_API_PORT || 3_100),
147
+ uiPort: Number(env.BIAWS_UI_PORT || 4_400),
146
148
  publicUrl:
147
149
  env.BIAWS_PUBLIC_URL ||
148
- `http://localhost:${env.ISSUE_UI_PORT || 4_400}`,
150
+ `http://localhost:${env.BIAWS_UI_PORT || 4_400}`,
149
151
  storage: Object.values(STORAGE_FIELDS).some((key) => env[key])
150
152
  ? "directories"
151
153
  : "volumes",
@@ -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
- { cwd: context.repositoryRoot, silent: true },
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) {