biaws 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +229 -0
  3. package/bin/biaws.js +3 -0
  4. package/docs/command-taxonomy.md +36 -0
  5. package/docs/instance-lifecycle.md +82 -0
  6. package/docs/releasing.md +105 -0
  7. package/package.json +63 -0
  8. package/shared/skillPackage.js +100 -0
  9. package/src/apiClient.js +89 -0
  10. package/src/baseCommands.js +122 -0
  11. package/src/commands/agent/configure.js +3 -0
  12. package/src/commands/agent/doctor.js +3 -0
  13. package/src/commands/agent.js +400 -0
  14. package/src/commands/api.js +17 -0
  15. package/src/commands/applications/get.js +17 -0
  16. package/src/commands/applications/list.js +32 -0
  17. package/src/commands/applications.js +7 -0
  18. package/src/commands/configure/claude.js +25 -0
  19. package/src/commands/configure/codex.js +25 -0
  20. package/src/commands/configure/doctor.js +49 -0
  21. package/src/commands/configure/skills.js +74 -0
  22. package/src/commands/configure.js +19 -0
  23. package/src/commands/demands/complete-task.js +6 -0
  24. package/src/commands/demands/get.js +16 -0
  25. package/src/commands/demands/list.js +48 -0
  26. package/src/commands/demands/task-status.js +49 -0
  27. package/src/commands/demands/tasks.js +44 -0
  28. package/src/commands/demands.js +7 -0
  29. package/src/commands/instance/backup.js +74 -0
  30. package/src/commands/instance/list.js +39 -0
  31. package/src/commands/instance/remove.js +72 -0
  32. package/src/commands/instance/restore.js +96 -0
  33. package/src/commands/instance/setup.js +349 -0
  34. package/src/commands/instance/show.js +31 -0
  35. package/src/commands/instance/start.js +32 -0
  36. package/src/commands/instance/status.js +42 -0
  37. package/src/commands/instance/stop.js +30 -0
  38. package/src/commands/instance.js +18 -0
  39. package/src/commands/issues/get.js +16 -0
  40. package/src/commands/issues/list.js +51 -0
  41. package/src/commands/issues/transition.js +33 -0
  42. package/src/commands/issues.js +7 -0
  43. package/src/commands/monitoring/describe.js +13 -0
  44. package/src/commands/monitoring/signal.js +25 -0
  45. package/src/commands/monitoring/signals.js +17 -0
  46. package/src/commands/monitoring/validate.js +14 -0
  47. package/src/commands/monitoring.js +163 -0
  48. package/src/commands/skills/install-all.js +13 -0
  49. package/src/commands/skills/install.js +16 -0
  50. package/src/commands/skills/list.js +5 -0
  51. package/src/commands/skills/publish-all.js +14 -0
  52. package/src/commands/skills/publish.js +11 -0
  53. package/src/commands/skills/status.js +7 -0
  54. package/src/commands/skills/update.js +12 -0
  55. package/src/commands/skills.js +303 -0
  56. package/src/commands/workspaces/get.js +15 -0
  57. package/src/commands/workspaces/list.js +20 -0
  58. package/src/commands/workspaces.js +7 -0
  59. package/src/compatibilityCommands.js +107 -0
  60. package/src/configure/command.js +50 -0
  61. package/src/core/context.js +162 -0
  62. package/src/core/errors.js +25 -0
  63. package/src/core/processRunner.js +119 -0
  64. package/src/core/prompts.js +138 -0
  65. package/src/core/redaction.js +29 -0
  66. package/src/core/terminal.js +72 -0
  67. package/src/core/wizard.js +279 -0
  68. package/src/domain/readCommand.js +65 -0
  69. package/src/domain/readService.js +126 -0
  70. package/src/domain/writeCommand.js +91 -0
  71. package/src/domain/writeService.js +122 -0
  72. package/src/index.js +5 -0
  73. package/src/instance/command.js +37 -0
  74. package/src/instance/service.js +515 -0
  75. package/src/localSkills.js +143 -0
  76. package/src/skillPackage.js +4 -0
@@ -0,0 +1,107 @@
1
+ import { Args, Flags, Help } from "@oclif/core";
2
+
3
+ import { AuthenticatedApiCommand, ProjectCommand } from "./baseCommands.js";
4
+ import {
5
+ configureContextFlags,
6
+ configureContextInput,
7
+ legacyAgentContext,
8
+ } from "./configure/command.js";
9
+ import { runAgentCommand } from "./commands/agent.js";
10
+ import { runMonitoringCommand } from "./commands/monitoring.js";
11
+ import { runSkillsCommand } from "./commands/skills.js";
12
+
13
+ export const authenticatedFlags = Object.freeze({
14
+ "api-key": Flags.string({
15
+ description: "chave da API (prefira ISSUE_API_KEY ou --env-file)",
16
+ }),
17
+ "api-url": Flags.string({ description: "URL da API do Bondia Workspaces" }),
18
+ "env-file": Flags.string({ description: "arquivo privado com URL e chave" }),
19
+ workspace: Flags.string({ char: "w", description: "workspace da operação" }),
20
+ json: Flags.boolean({ description: "emite somente JSON em stdout" }),
21
+ });
22
+
23
+ function authenticatedInput(flags) {
24
+ return {
25
+ apiKey: flags["api-key"],
26
+ apiUrl: flags["api-url"],
27
+ envFile: flags["env-file"],
28
+ workspace: flags.workspace,
29
+ };
30
+ }
31
+
32
+ export function createSkillsCommand(action, definition = {}) {
33
+ return class SkillsAction extends AuthenticatedApiCommand {
34
+ static description = definition.description;
35
+ static args = definition.args || {};
36
+ static flags = { ...authenticatedFlags, ...(definition.flags || {}) };
37
+
38
+ async run() {
39
+ const { args, flags } = await this.parse(this.constructor);
40
+ const context = await this.authenticatedContext(
41
+ authenticatedInput(flags),
42
+ );
43
+ const positional = definition.positional
44
+ ? definition.positional(args)
45
+ : [];
46
+ await runSkillsCommand(context.api, action, positional, flags);
47
+ }
48
+ };
49
+ }
50
+
51
+ export function createMonitoringCommand(action, definition = {}) {
52
+ return class MonitoringAction extends AuthenticatedApiCommand {
53
+ static description = definition.description;
54
+ static args = definition.args || {};
55
+ static flags = { ...authenticatedFlags, ...(definition.flags || {}) };
56
+
57
+ async run() {
58
+ const { args, flags } = await this.parse(this.constructor);
59
+ const context = await this.authenticatedContext(
60
+ authenticatedInput(flags),
61
+ );
62
+ const positional = definition.positional
63
+ ? definition.positional(args)
64
+ : [];
65
+ await runMonitoringCommand(context.api, action, positional, flags);
66
+ }
67
+ };
68
+ }
69
+
70
+ export function createAgentCommand(action) {
71
+ return class AgentAction extends ProjectCommand {
72
+ static description = `${action === "configure" ? "configura" : "diagnostica"} Codex ou Claude (alias legado)`;
73
+ static args = {
74
+ client: Args.string({
75
+ description: "cliente de agente",
76
+ options: ["codex", "claude"],
77
+ required: true,
78
+ }),
79
+ };
80
+ static flags = configureContextFlags;
81
+
82
+ async run() {
83
+ const { args, flags } = await this.parse(this.constructor);
84
+ const context = await this.projectContext(configureContextInput(flags));
85
+ await runAgentCommand(
86
+ context.api,
87
+ action,
88
+ [args.client],
89
+ { ...flags, project: context.projectDirectory },
90
+ legacyAgentContext(context),
91
+ );
92
+ }
93
+ };
94
+ }
95
+
96
+ export async function showTopic(command, topic) {
97
+ await command.parse(command.constructor);
98
+ await new Help(command.config).showHelp([topic]);
99
+ }
100
+
101
+ export const optionalSkillArgument = Args.string({
102
+ description: "ID da skill",
103
+ });
104
+ export const runtimeArgument = Args.string({
105
+ description: "UUID ou caminho aplicação.componente.deployment.runtime",
106
+ required: true,
107
+ });
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+
3
+ import { Args, Flags } from "@oclif/core";
4
+
5
+ export const configureContextFlags = Object.freeze({
6
+ project: Flags.string({
7
+ char: "p",
8
+ description: "diretório do projeto a configurar",
9
+ }),
10
+ workspace: Flags.string({
11
+ char: "w",
12
+ description: "workspace associado ao projeto",
13
+ }),
14
+ "api-url": Flags.string({ description: "URL da API do Bondia Workspaces" }),
15
+ "env-file": Flags.string({
16
+ description: "arquivo privado com URL e chave da API",
17
+ }),
18
+ force: Flags.boolean({
19
+ char: "f",
20
+ description: "substitui somente a configuração BIAWS conflitante",
21
+ }),
22
+ json: Flags.boolean({ description: "emite somente JSON em stdout" }),
23
+ });
24
+
25
+ export const clientArgument = Args.string({
26
+ description: "cliente a diagnosticar",
27
+ options: ["codex", "claude"],
28
+ required: false,
29
+ });
30
+
31
+ export function configureContextInput(flags) {
32
+ return {
33
+ apiUrl: flags["api-url"],
34
+ envFile: flags["env-file"],
35
+ project: flags.project,
36
+ workspace: flags.workspace,
37
+ };
38
+ }
39
+
40
+ export function legacyAgentContext(context) {
41
+ return {
42
+ apiKey: context.apiKey,
43
+ apiUrl: context.apiUrl,
44
+ envFile: context.envFile,
45
+ toolDirectory: context.repositoryRoot
46
+ ? path.join(context.repositoryRoot, "biaws-cli")
47
+ : context.toolDirectory,
48
+ workspaceId: context.workspaceId,
49
+ };
50
+ }
@@ -0,0 +1,162 @@
1
+ import path from "node:path";
2
+
3
+ import { CliError } from "./errors.js";
4
+
5
+ const DEFAULT_API_URL = "http://127.0.0.1:3100";
6
+
7
+ export function parseEnv(contents) {
8
+ const parsed = {};
9
+ for (const rawLine of String(contents || "").split(/\r?\n/u)) {
10
+ const line = rawLine.trim();
11
+ if (!line || line.startsWith("#")) continue;
12
+ const separator = line.indexOf("=");
13
+ if (separator < 1) continue;
14
+ const key = line.slice(0, separator).trim();
15
+ let value = line.slice(separator + 1).trim();
16
+ if (
17
+ (value.startsWith('"') && value.endsWith('"')) ||
18
+ (value.startsWith("'") && value.endsWith("'"))
19
+ ) {
20
+ value = value.slice(1, -1);
21
+ }
22
+ parsed[key] = value.replace(/\\n/gu, "\n");
23
+ }
24
+ return parsed;
25
+ }
26
+
27
+ async function readEnvFile(filesystem, filePath) {
28
+ if (!filePath) return {};
29
+ try {
30
+ return parseEnv(await filesystem.readFile(filePath, "utf8"));
31
+ } catch (error) {
32
+ if (error.code === "ENOENT") return {};
33
+ throw new CliError(`Não foi possível ler a configuração em ${filePath}.`, {
34
+ code: "ENV_FILE_READ_FAILED",
35
+ cause: error,
36
+ });
37
+ }
38
+ }
39
+
40
+ function normalizeApiUrl(value) {
41
+ let parsed;
42
+ try {
43
+ parsed = new URL(String(value));
44
+ } catch {
45
+ throw new CliError("A URL da API é inválida.", {
46
+ code: "INVALID_API_URL",
47
+ exitCode: 2,
48
+ });
49
+ }
50
+ if (
51
+ !["http:", "https:"].includes(parsed.protocol) ||
52
+ parsed.username ||
53
+ parsed.password
54
+ ) {
55
+ throw new CliError(
56
+ "A URL da API deve usar HTTP(S) e não conter credenciais.",
57
+ {
58
+ code: "INVALID_API_URL",
59
+ exitCode: 2,
60
+ },
61
+ );
62
+ }
63
+ parsed.password = "";
64
+ return parsed.toString().replace(/\/+$/u, "");
65
+ }
66
+
67
+ export async function resolveCommandContext(options) {
68
+ const input = options.input || {};
69
+ const environment = { ...(options.environment || {}) };
70
+ const toolDirectory = path.resolve(options.toolDirectory);
71
+ const repositoryRoot = path.resolve(
72
+ input.root || environment.BIAWS_ROOT || toolDirectory,
73
+ input.root || environment.BIAWS_ROOT ? "." : "..",
74
+ );
75
+ const instancesDirectory = path.resolve(
76
+ input.instancesDirectory ||
77
+ environment.BIAWS_INSTANCES_DIR ||
78
+ path.join(repositoryRoot, "instances"),
79
+ );
80
+ const instanceName = String(
81
+ input.instance || environment.BIAWS_INSTANCE || "",
82
+ ).trim();
83
+ const instanceDirectory = path.resolve(
84
+ input.instanceDirectory ||
85
+ environment.BIAWS_INSTANCE_DIR ||
86
+ (instanceName
87
+ ? path.join(instancesDirectory, instanceName)
88
+ : instancesDirectory),
89
+ );
90
+ const explicitEnvFile = String(
91
+ input.envFile || environment.BIAWS_ENV_FILE || "",
92
+ ).trim();
93
+ const envFile = explicitEnvFile
94
+ ? path.resolve(explicitEnvFile)
95
+ : instanceName
96
+ ? path.join(instanceDirectory, ".env")
97
+ : "";
98
+ const rootEnv = await readEnvFile(
99
+ options.filesystem,
100
+ path.join(repositoryRoot, ".env"),
101
+ );
102
+ const toolEnv = await readEnvFile(
103
+ options.filesystem,
104
+ path.join(toolDirectory, ".env"),
105
+ );
106
+ const instanceEnv = await readEnvFile(options.filesystem, envFile);
107
+ const effectiveEnv = {
108
+ ...rootEnv,
109
+ ...environment,
110
+ ...toolEnv,
111
+ ...instanceEnv,
112
+ };
113
+ const apiUrl = normalizeApiUrl(
114
+ input.apiUrl ||
115
+ effectiveEnv.ISSUE_API_URL ||
116
+ effectiveEnv.ISSUE_API_BASE_URL ||
117
+ DEFAULT_API_URL,
118
+ );
119
+ const apiKey = String(input.apiKey || effectiveEnv.ISSUE_API_KEY || "");
120
+ const workspaceId = String(
121
+ input.workspaceId ||
122
+ input.workspace ||
123
+ effectiveEnv.ISSUE_WORKSPACE_ID ||
124
+ "",
125
+ ).trim();
126
+ const projectDirectory = path.resolve(
127
+ input.project || options.cwd || repositoryRoot,
128
+ );
129
+
130
+ return Object.freeze({
131
+ apiKey,
132
+ apiUrl,
133
+ env: Object.freeze(effectiveEnv),
134
+ envFile,
135
+ instanceDirectory,
136
+ instanceName,
137
+ instancesDirectory,
138
+ isCI: Boolean(options.terminal.isCI),
139
+ isInteractive: Boolean(options.terminal.isInteractive),
140
+ projectDirectory,
141
+ repositoryRoot,
142
+ toolDirectory,
143
+ workspaceId,
144
+ });
145
+ }
146
+
147
+ export async function resolveAuthenticatedContext(options) {
148
+ const context = await resolveCommandContext(options);
149
+ if (!context.apiKey) {
150
+ throw new CliError(
151
+ "Chave da API ausente. Defina ISSUE_API_KEY em um ambiente privado.",
152
+ { code: "AUTHENTICATION_REQUIRED", exitCode: 2 },
153
+ );
154
+ }
155
+ if (options.requireWorkspace && !context.workspaceId) {
156
+ throw new CliError(
157
+ "Workspace ausente. Informe --workspace ou defina ISSUE_WORKSPACE_ID.",
158
+ { code: "WORKSPACE_REQUIRED", exitCode: 2 },
159
+ );
160
+ }
161
+ return context;
162
+ }
@@ -0,0 +1,25 @@
1
+ export class CliError extends Error {
2
+ constructor(message, options = {}) {
3
+ super(message, { cause: options.cause });
4
+ this.name = "CliError";
5
+ this.code = options.code || "CLI_ERROR";
6
+ this.exitCode = options.exitCode ?? 1;
7
+ this.oclif = { exit: this.exitCode };
8
+ this.details = options.details;
9
+ }
10
+ }
11
+
12
+ export class ProcessExecutionError extends CliError {
13
+ constructor(message, options = {}) {
14
+ super(message, {
15
+ ...options,
16
+ code: options.code || "PROCESS_EXECUTION_FAILED",
17
+ exitCode: options.exitCode ?? 1,
18
+ });
19
+ this.name = "ProcessExecutionError";
20
+ this.command = options.command;
21
+ this.processExitCode = options.processExitCode ?? null;
22
+ this.signal = options.signal ?? null;
23
+ this.stderr = options.stderr || "";
24
+ }
25
+ }
@@ -0,0 +1,119 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+
3
+ import { ProcessExecutionError } from "./errors.js";
4
+ import { redactText } from "./redaction.js";
5
+
6
+ function attachOutput(stream, target, chunks, shouldBuffer) {
7
+ if (!stream) return;
8
+ stream.on("data", (chunk) => {
9
+ const value = String(chunk);
10
+ chunks.push(value);
11
+ if (!shouldBuffer && target) target.write(value);
12
+ });
13
+ }
14
+
15
+ export class ProcessRunner {
16
+ constructor(options = {}) {
17
+ this.spawn = options.spawn || nodeSpawn;
18
+ this.signalSource = options.signalSource || process;
19
+ this.stdout = options.stdout || process.stdout;
20
+ this.stderr = options.stderr || process.stderr;
21
+ }
22
+
23
+ run(command, args = [], options = {}) {
24
+ if (typeof command !== "string" || !command || !Array.isArray(args)) {
25
+ throw new TypeError("ProcessRunner exige comando e array de argumentos.");
26
+ }
27
+ const secrets = options.secrets || [];
28
+ const shouldBuffer =
29
+ Boolean(options.silent) || secrets.some((secret) => String(secret || ""));
30
+ const stdoutChunks = [];
31
+ const stderrChunks = [];
32
+ let child;
33
+ try {
34
+ child = this.spawn(command, args.map(String), {
35
+ cwd: options.cwd,
36
+ env: options.env,
37
+ shell: false,
38
+ stdio: [
39
+ options.input === undefined ? "ignore" : "pipe",
40
+ "pipe",
41
+ "pipe",
42
+ ],
43
+ });
44
+ } catch (cause) {
45
+ throw new ProcessExecutionError(
46
+ `Falha ao iniciar ${redactText(command, secrets)}.`,
47
+ { cause, command: redactText(command, secrets) },
48
+ );
49
+ }
50
+ attachOutput(child.stdout, this.stdout, stdoutChunks, shouldBuffer);
51
+ attachOutput(child.stderr, this.stderr, stderrChunks, shouldBuffer);
52
+ if (options.input !== undefined) child.stdin.end(options.input);
53
+
54
+ return new Promise((resolve, reject) => {
55
+ const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
56
+ const handlers = new Map(
57
+ forwardedSignals.map((signal) => [
58
+ signal,
59
+ () => {
60
+ if (!child.killed) child.kill(signal);
61
+ },
62
+ ]),
63
+ );
64
+ for (const [signal, handler] of handlers) {
65
+ this.signalSource.on?.(signal, handler);
66
+ }
67
+ const cleanup = () => {
68
+ for (const [signal, handler] of handlers) {
69
+ this.signalSource.off?.(signal, handler);
70
+ }
71
+ };
72
+ const output = () => ({
73
+ stdout: redactText(stdoutChunks.join(""), secrets),
74
+ stderr: redactText(stderrChunks.join(""), secrets),
75
+ });
76
+ const flush = (result) => {
77
+ if (!shouldBuffer || options.silent) return;
78
+ if (result.stdout && this.stdout) this.stdout.write(result.stdout);
79
+ if (result.stderr && this.stderr) this.stderr.write(result.stderr);
80
+ };
81
+
82
+ child.once("error", (cause) => {
83
+ cleanup();
84
+ reject(
85
+ new ProcessExecutionError(
86
+ `Falha ao iniciar ${redactText(command, secrets)}.`,
87
+ { cause, command: redactText(command, secrets) },
88
+ ),
89
+ );
90
+ });
91
+ child.once("close", (processExitCode, signal) => {
92
+ cleanup();
93
+ const result = { ...output(), processExitCode, signal: signal || null };
94
+ flush(result);
95
+ if (processExitCode === 0 && !signal) {
96
+ resolve(result);
97
+ return;
98
+ }
99
+ const renderedCommand = redactText(
100
+ [command, ...args].join(" "),
101
+ secrets,
102
+ );
103
+ reject(
104
+ new ProcessExecutionError(
105
+ signal
106
+ ? `Processo interrompido por ${signal}: ${renderedCommand}`
107
+ : `Processo encerrou com código ${processExitCode}: ${renderedCommand}`,
108
+ {
109
+ command: renderedCommand,
110
+ processExitCode,
111
+ signal,
112
+ stderr: result.stderr,
113
+ },
114
+ ),
115
+ );
116
+ });
117
+ });
118
+ }
119
+ }
@@ -0,0 +1,138 @@
1
+ import { confirm, input, number, password, select } from "@inquirer/prompts";
2
+
3
+ import { CliError } from "./errors.js";
4
+
5
+ export class PromptCancelledError extends CliError {
6
+ constructor(
7
+ message = "Operação cancelada antes de qualquer alteração.",
8
+ options = {},
9
+ ) {
10
+ super(message, {
11
+ ...options,
12
+ code: "PROMPT_CANCELLED",
13
+ exitCode: options.exitCode ?? 130,
14
+ });
15
+ this.name = "PromptCancelledError";
16
+ }
17
+ }
18
+
19
+ function isPromptCancellation(error) {
20
+ return (
21
+ error?.name === "ExitPromptError" ||
22
+ error?.name === "AbortPromptError" ||
23
+ error?.name === "AbortError"
24
+ );
25
+ }
26
+
27
+ export class PromptAdapter {
28
+ constructor(options = {}) {
29
+ this.isInteractive = Boolean(options.isInteractive);
30
+ }
31
+
32
+ async ask() {
33
+ throw new CliError("O adapter de prompt não implementa ask().", {
34
+ code: "PROMPT_ADAPTER_INVALID",
35
+ });
36
+ }
37
+ }
38
+
39
+ export class InteractivePromptAdapter extends PromptAdapter {
40
+ constructor(options = {}) {
41
+ super({ isInteractive: true });
42
+ this.prompts = options.prompts || {
43
+ confirm,
44
+ input,
45
+ number,
46
+ password,
47
+ select,
48
+ };
49
+ this.context = options.context || {};
50
+ }
51
+
52
+ async ask(question, state = {}) {
53
+ const handler = this.prompts[question.type || "input"];
54
+ if (!handler) {
55
+ throw new CliError(`Tipo de pergunta não suportado: ${question.type}.`, {
56
+ code: "PROMPT_TYPE_UNSUPPORTED",
57
+ exitCode: 2,
58
+ });
59
+ }
60
+ const config = {
61
+ message: question.message || question.name,
62
+ ...(question.choices ? { choices: question.choices } : {}),
63
+ ...(Object.hasOwn(question, "default")
64
+ ? { default: question.default }
65
+ : {}),
66
+ };
67
+ try {
68
+ return await handler(config, {
69
+ ...this.context,
70
+ signal: state.signal || this.context.signal,
71
+ });
72
+ } catch (error) {
73
+ if (isPromptCancellation(error)) throw new PromptCancelledError();
74
+ throw error;
75
+ }
76
+ }
77
+ }
78
+
79
+ export class NonInteractivePromptAdapter extends PromptAdapter {
80
+ constructor() {
81
+ super({ isInteractive: false });
82
+ }
83
+
84
+ async ask() {
85
+ throw new CliError(
86
+ "Não é possível solicitar entrada sem terminal interativo.",
87
+ {
88
+ code: "INTERACTIVE_INPUT_UNAVAILABLE",
89
+ exitCode: 2,
90
+ },
91
+ );
92
+ }
93
+ }
94
+
95
+ export class ProgrammedPromptAdapter extends PromptAdapter {
96
+ constructor(responses = {}, options = {}) {
97
+ super({ isInteractive: options.isInteractive ?? true });
98
+ this.responses = Array.isArray(responses)
99
+ ? [...responses]
100
+ : { ...responses };
101
+ this.questions = [];
102
+ }
103
+
104
+ async ask(question, state = {}) {
105
+ this.questions.push(question.name);
106
+ let response;
107
+ if (Array.isArray(this.responses)) {
108
+ if (this.responses.length === 0) {
109
+ throw new CliError(
110
+ `Resposta programada ausente para ${question.name}.`,
111
+ {
112
+ code: "PROGRAMMED_RESPONSE_MISSING",
113
+ },
114
+ );
115
+ }
116
+ response = this.responses.shift();
117
+ } else if (Object.hasOwn(this.responses, question.name)) {
118
+ response = this.responses[question.name];
119
+ } else {
120
+ throw new CliError(`Resposta programada ausente para ${question.name}.`, {
121
+ code: "PROGRAMMED_RESPONSE_MISSING",
122
+ });
123
+ }
124
+ if (typeof response === "function")
125
+ response = await response(question, state);
126
+ if (response instanceof Error) throw response;
127
+ return response;
128
+ }
129
+ }
130
+
131
+ export function createPromptAdapter(terminal, options = {}) {
132
+ if (options.adapter) return options.adapter;
133
+ if (!terminal?.isInteractive) return new NonInteractivePromptAdapter();
134
+ return new InteractivePromptAdapter({
135
+ context: { input: terminal.stdin, output: terminal.stdout },
136
+ prompts: options.prompts,
137
+ });
138
+ }
@@ -0,0 +1,29 @@
1
+ const SECRET_KEY_PATTERN =
2
+ /(?:api[-_]?key|authorization|cookie|credential|password|secret|token)/iu;
3
+
4
+ export function redactText(value, secrets = []) {
5
+ let output = String(value ?? "");
6
+ for (const secret of secrets) {
7
+ const normalized = String(secret ?? "");
8
+ if (normalized) output = output.split(normalized).join("[REDACTED]");
9
+ }
10
+ return output;
11
+ }
12
+
13
+ export function redactValue(value, secrets = [], seen = new WeakSet()) {
14
+ if (typeof value === "string") return redactText(value, secrets);
15
+ if (value === null || typeof value !== "object") return value;
16
+ if (seen.has(value)) return "[CIRCULAR]";
17
+ seen.add(value);
18
+ if (Array.isArray(value)) {
19
+ return value.map((item) => redactValue(item, secrets, seen));
20
+ }
21
+ return Object.fromEntries(
22
+ Object.entries(value).map(([key, item]) => [
23
+ key,
24
+ SECRET_KEY_PATTERN.test(key)
25
+ ? "[REDACTED]"
26
+ : redactValue(item, secrets, seen),
27
+ ]),
28
+ );
29
+ }
@@ -0,0 +1,72 @@
1
+ import { redactText, redactValue } from "./redaction.js";
2
+
3
+ function writeLine(stream, value) {
4
+ stream.write(`${value}\n`);
5
+ }
6
+
7
+ export function createTerminalAdapter(options = {}) {
8
+ const stdin = options.stdin || process.stdin;
9
+ const stdout = options.stdout || process.stdout;
10
+ const stderr = options.stderr || process.stderr;
11
+ const environment = options.environment || process.env;
12
+ return Object.freeze({
13
+ stdin,
14
+ stdout,
15
+ stderr,
16
+ isInteractive: Boolean(stdin.isTTY && stdout.isTTY),
17
+ isCI: Boolean(environment.CI),
18
+ });
19
+ }
20
+
21
+ export class CliOutput {
22
+ constructor(terminal, options = {}) {
23
+ this.terminal = terminal;
24
+ this.json = Boolean(options.json);
25
+ this.secrets = options.secrets || [];
26
+ }
27
+
28
+ result(value) {
29
+ if (this.json) {
30
+ writeLine(
31
+ this.terminal.stdout,
32
+ JSON.stringify(redactValue(value, this.secrets), null, 2),
33
+ );
34
+ return;
35
+ }
36
+ writeLine(this.terminal.stdout, redactText(value, this.secrets));
37
+ }
38
+
39
+ diagnostic(value) {
40
+ writeLine(this.terminal.stderr, redactText(value, this.secrets));
41
+ }
42
+ }
43
+
44
+ export class CliLogger {
45
+ constructor(terminal, options = {}) {
46
+ this.terminal = terminal;
47
+ this.secrets = options.secrets || [];
48
+ this.verbose = Boolean(options.verbose);
49
+ }
50
+
51
+ event(level, event, fields = {}) {
52
+ if (level === "debug" && !this.verbose) return;
53
+ const entry = redactValue({ ...fields, level, event }, this.secrets);
54
+ writeLine(this.terminal.stderr, JSON.stringify(entry));
55
+ }
56
+
57
+ debug(event, fields) {
58
+ this.event("debug", event, fields);
59
+ }
60
+
61
+ info(event, fields) {
62
+ this.event("info", event, fields);
63
+ }
64
+
65
+ warn(event, fields) {
66
+ this.event("warn", event, fields);
67
+ }
68
+
69
+ error(event, fields) {
70
+ this.event("error", event, fields);
71
+ }
72
+ }