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.
- package/LICENSE +201 -0
- package/README.md +229 -0
- package/bin/biaws.js +3 -0
- package/docs/command-taxonomy.md +36 -0
- package/docs/instance-lifecycle.md +82 -0
- package/docs/releasing.md +105 -0
- package/package.json +63 -0
- package/shared/skillPackage.js +100 -0
- package/src/apiClient.js +89 -0
- package/src/baseCommands.js +122 -0
- package/src/commands/agent/configure.js +3 -0
- package/src/commands/agent/doctor.js +3 -0
- package/src/commands/agent.js +400 -0
- package/src/commands/api.js +17 -0
- package/src/commands/applications/get.js +17 -0
- package/src/commands/applications/list.js +32 -0
- package/src/commands/applications.js +7 -0
- package/src/commands/configure/claude.js +25 -0
- package/src/commands/configure/codex.js +25 -0
- package/src/commands/configure/doctor.js +49 -0
- package/src/commands/configure/skills.js +74 -0
- package/src/commands/configure.js +19 -0
- package/src/commands/demands/complete-task.js +6 -0
- package/src/commands/demands/get.js +16 -0
- package/src/commands/demands/list.js +48 -0
- package/src/commands/demands/task-status.js +49 -0
- package/src/commands/demands/tasks.js +44 -0
- package/src/commands/demands.js +7 -0
- package/src/commands/instance/backup.js +74 -0
- package/src/commands/instance/list.js +39 -0
- package/src/commands/instance/remove.js +72 -0
- package/src/commands/instance/restore.js +96 -0
- package/src/commands/instance/setup.js +349 -0
- package/src/commands/instance/show.js +31 -0
- package/src/commands/instance/start.js +32 -0
- package/src/commands/instance/status.js +42 -0
- package/src/commands/instance/stop.js +30 -0
- package/src/commands/instance.js +18 -0
- package/src/commands/issues/get.js +16 -0
- package/src/commands/issues/list.js +51 -0
- package/src/commands/issues/transition.js +33 -0
- package/src/commands/issues.js +7 -0
- package/src/commands/monitoring/describe.js +13 -0
- package/src/commands/monitoring/signal.js +25 -0
- package/src/commands/monitoring/signals.js +17 -0
- package/src/commands/monitoring/validate.js +14 -0
- package/src/commands/monitoring.js +163 -0
- package/src/commands/skills/install-all.js +13 -0
- package/src/commands/skills/install.js +16 -0
- package/src/commands/skills/list.js +5 -0
- package/src/commands/skills/publish-all.js +14 -0
- package/src/commands/skills/publish.js +11 -0
- package/src/commands/skills/status.js +7 -0
- package/src/commands/skills/update.js +12 -0
- package/src/commands/skills.js +303 -0
- package/src/commands/workspaces/get.js +15 -0
- package/src/commands/workspaces/list.js +20 -0
- package/src/commands/workspaces.js +7 -0
- package/src/compatibilityCommands.js +107 -0
- package/src/configure/command.js +50 -0
- package/src/core/context.js +162 -0
- package/src/core/errors.js +25 -0
- package/src/core/processRunner.js +119 -0
- package/src/core/prompts.js +138 -0
- package/src/core/redaction.js +29 -0
- package/src/core/terminal.js +72 -0
- package/src/core/wizard.js +279 -0
- package/src/domain/readCommand.js +65 -0
- package/src/domain/readService.js +126 -0
- package/src/domain/writeCommand.js +91 -0
- package/src/domain/writeService.js +122 -0
- package/src/index.js +5 -0
- package/src/instance/command.js +37 -0
- package/src/instance/service.js +515 -0
- package/src/localSkills.js +143 -0
- package/src/skillPackage.js +4 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
NonInteractivePromptAdapter,
|
|
6
|
+
PromptCancelledError,
|
|
7
|
+
createPromptAdapter,
|
|
8
|
+
} from "./prompts.js";
|
|
9
|
+
import { redactValue } from "./redaction.js";
|
|
10
|
+
|
|
11
|
+
const REDACTED = "[REDACTED]";
|
|
12
|
+
|
|
13
|
+
export const wizardFlags = Object.freeze({
|
|
14
|
+
defaults: Flags.boolean({
|
|
15
|
+
description: "aplica somente os defaults declarados pelo comando",
|
|
16
|
+
}),
|
|
17
|
+
interactive: Flags.boolean({
|
|
18
|
+
description: "solicita em um TTY os campos que não foram informados",
|
|
19
|
+
exclusive: ["non-interactive"],
|
|
20
|
+
}),
|
|
21
|
+
json: Flags.boolean({
|
|
22
|
+
description: "emite o resumo estruturado em JSON",
|
|
23
|
+
}),
|
|
24
|
+
"non-interactive": Flags.boolean({
|
|
25
|
+
description: "não solicita entradas e falha se campos estiverem ausentes",
|
|
26
|
+
exclusive: ["interactive"],
|
|
27
|
+
}),
|
|
28
|
+
yes: Flags.boolean({
|
|
29
|
+
char: "y",
|
|
30
|
+
description: "pula somente a confirmação do plano validado",
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function hasValue(value) {
|
|
35
|
+
return value !== undefined && value !== null && value !== "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clone(value) {
|
|
39
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
Object.entries(value).map(([key, item]) => [key, clone(item)]),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deepFreeze(value) {
|
|
49
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value))
|
|
50
|
+
return value;
|
|
51
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
52
|
+
return Object.freeze(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class MissingWizardInputError extends CliError {
|
|
56
|
+
constructor(fields) {
|
|
57
|
+
const names = fields.map((field) => field.name);
|
|
58
|
+
super(`Campos obrigatórios ausentes: ${names.join(", ")}.`, {
|
|
59
|
+
code: "MISSING_REQUIRED_INPUT",
|
|
60
|
+
exitCode: 2,
|
|
61
|
+
details: { fields: names },
|
|
62
|
+
});
|
|
63
|
+
this.name = "MissingWizardInputError";
|
|
64
|
+
this.fields = names;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ExecutionPlan {
|
|
69
|
+
#values;
|
|
70
|
+
|
|
71
|
+
constructor(kind, values, questions) {
|
|
72
|
+
this.#values = deepFreeze(clone(values));
|
|
73
|
+
const secretNames = new Set(
|
|
74
|
+
questions
|
|
75
|
+
.filter((question) => question.secret)
|
|
76
|
+
.map((question) => question.name),
|
|
77
|
+
);
|
|
78
|
+
const secretValues = [...secretNames]
|
|
79
|
+
.map((name) => values[name])
|
|
80
|
+
.filter(hasValue)
|
|
81
|
+
.map(String);
|
|
82
|
+
this.kind = kind;
|
|
83
|
+
this.values = deepFreeze(
|
|
84
|
+
Object.fromEntries(
|
|
85
|
+
Object.entries(values).map(([name, value]) => [
|
|
86
|
+
name,
|
|
87
|
+
secretNames.has(name) && hasValue(value)
|
|
88
|
+
? REDACTED
|
|
89
|
+
: redactValue(clone(value), secretValues),
|
|
90
|
+
]),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
Object.freeze(this);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get(name) {
|
|
97
|
+
return this.#values[name];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
toJSON() {
|
|
101
|
+
return { kind: this.kind, values: this.values };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function normalizeWizardOptions(options = {}, terminal = {}) {
|
|
106
|
+
const nonInteractive = Boolean(
|
|
107
|
+
options.nonInteractive || options["non-interactive"],
|
|
108
|
+
);
|
|
109
|
+
if (options.interactive && nonInteractive) {
|
|
110
|
+
throw new CliError(
|
|
111
|
+
"--interactive e --non-interactive são mutuamente exclusivos.",
|
|
112
|
+
{
|
|
113
|
+
code: "INTERACTION_MODE_CONFLICT",
|
|
114
|
+
exitCode: 2,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
defaults: Boolean(options.defaults),
|
|
120
|
+
interactive: Boolean(
|
|
121
|
+
!nonInteractive &&
|
|
122
|
+
terminal.isInteractive &&
|
|
123
|
+
(options.interactive || !nonInteractive),
|
|
124
|
+
),
|
|
125
|
+
json: Boolean(options.json),
|
|
126
|
+
yes: Boolean(options.yes),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sourceValue(question, flags, environment) {
|
|
131
|
+
const flagName = question.flag || question.name;
|
|
132
|
+
if (hasValue(flags[flagName])) return flags[flagName];
|
|
133
|
+
if (question.environment && hasValue(environment[question.environment])) {
|
|
134
|
+
return environment[question.environment];
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function isApplicable(question, values) {
|
|
140
|
+
return question.when
|
|
141
|
+
? Boolean(await question.when(Object.freeze({ ...values })))
|
|
142
|
+
: true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function validateAnswer(question, value, values) {
|
|
146
|
+
if (!question.validate) return;
|
|
147
|
+
const result = await question.validate(value, Object.freeze({ ...values }));
|
|
148
|
+
if (result === true || result === undefined) return;
|
|
149
|
+
const message =
|
|
150
|
+
typeof result === "string"
|
|
151
|
+
? result
|
|
152
|
+
: `Valor inválido para ${question.name}.`;
|
|
153
|
+
throw new CliError(message, {
|
|
154
|
+
code: "WIZARD_VALIDATION_FAILED",
|
|
155
|
+
exitCode: 2,
|
|
156
|
+
details: { field: question.name },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function collectWizardValues(definition, input = {}) {
|
|
161
|
+
const terminal = input.terminal || {};
|
|
162
|
+
const options = normalizeWizardOptions(input.options, terminal);
|
|
163
|
+
const adapter =
|
|
164
|
+
input.promptAdapter ||
|
|
165
|
+
(options.interactive
|
|
166
|
+
? createPromptAdapter(terminal, input.promptOptions)
|
|
167
|
+
: new NonInteractivePromptAdapter());
|
|
168
|
+
const values = {};
|
|
169
|
+
const missing = [];
|
|
170
|
+
const questions = definition.questions || [];
|
|
171
|
+
|
|
172
|
+
for (const question of questions) {
|
|
173
|
+
if (!(await isApplicable(question, values))) continue;
|
|
174
|
+
let value = sourceValue(
|
|
175
|
+
question,
|
|
176
|
+
input.flags || {},
|
|
177
|
+
input.environment || {},
|
|
178
|
+
);
|
|
179
|
+
const hasDefault = Object.hasOwn(question, "default");
|
|
180
|
+
const defaultValue = hasDefault
|
|
181
|
+
? typeof question.default === "function"
|
|
182
|
+
? await question.default(Object.freeze({ ...values }))
|
|
183
|
+
: question.default
|
|
184
|
+
: undefined;
|
|
185
|
+
if (!hasValue(value) && options.defaults && hasDefault) {
|
|
186
|
+
value = defaultValue;
|
|
187
|
+
}
|
|
188
|
+
if (!hasValue(value) && options.interactive) {
|
|
189
|
+
try {
|
|
190
|
+
value = await adapter.ask(
|
|
191
|
+
hasDefault ? { ...question, default: defaultValue } : question,
|
|
192
|
+
{
|
|
193
|
+
signal: input.signal,
|
|
194
|
+
values: Object.freeze({ ...values }),
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof PromptCancelledError) throw error;
|
|
199
|
+
if (error?.name === "AbortError") throw new PromptCancelledError();
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!hasValue(value)) {
|
|
204
|
+
if (question.required !== false) missing.push(question);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
await validateAnswer(question, value, values);
|
|
208
|
+
values[question.name] = value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (missing.length > 0) throw new MissingWizardInputError(missing);
|
|
212
|
+
return Object.freeze({ options, questions, values: deepFreeze(values) });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createExecutionPlan(definition, collected) {
|
|
216
|
+
if (!definition?.kind) {
|
|
217
|
+
throw new CliError("O wizard deve definir um identificador de plano.", {
|
|
218
|
+
code: "WIZARD_DEFINITION_INVALID",
|
|
219
|
+
exitCode: 2,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return new ExecutionPlan(
|
|
223
|
+
definition.kind,
|
|
224
|
+
collected.values,
|
|
225
|
+
collected.questions,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function summarizeExecutionPlan(plan) {
|
|
230
|
+
return plan.toJSON();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function confirmExecutionPlan(plan, input = {}) {
|
|
234
|
+
if (input.options?.yes) return true;
|
|
235
|
+
if (!input.terminal?.isInteractive || !input.promptAdapter?.isInteractive) {
|
|
236
|
+
throw new CliError(
|
|
237
|
+
"Confirmação necessária. Use --yes em modo não interativo.",
|
|
238
|
+
{
|
|
239
|
+
code: "CONFIRMATION_REQUIRED",
|
|
240
|
+
exitCode: 2,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
let confirmed;
|
|
245
|
+
try {
|
|
246
|
+
confirmed = await input.promptAdapter.ask(
|
|
247
|
+
{
|
|
248
|
+
name: "confirmation",
|
|
249
|
+
type: "confirm",
|
|
250
|
+
message: input.message || `Executar o plano ${plan.kind}?`,
|
|
251
|
+
default: false,
|
|
252
|
+
},
|
|
253
|
+
{ signal: input.signal, values: plan.values },
|
|
254
|
+
);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof PromptCancelledError) throw error;
|
|
257
|
+
if (error?.name === "AbortError") throw new PromptCancelledError();
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
if (!confirmed) throw new PromptCancelledError();
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function executeExecutionPlan(plan, executor, input = {}) {
|
|
265
|
+
if (typeof executor !== "function") {
|
|
266
|
+
throw new CliError("Executor do plano não informado.", {
|
|
267
|
+
code: "PLAN_EXECUTOR_INVALID",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
await confirmExecutionPlan(plan, input);
|
|
271
|
+
return executor(plan, { signal: input.signal });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function buildExecutionPlan(definition, input = {}) {
|
|
275
|
+
return createExecutionPlan(
|
|
276
|
+
definition,
|
|
277
|
+
await collectWizardValues(definition, input),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import { AuthenticatedApiCommand } from "../baseCommands.js";
|
|
3
|
+
import { asCliError, DomainReadService, readEnvelope } from "./readService.js";
|
|
4
|
+
|
|
5
|
+
export const connectionFlags = {
|
|
6
|
+
"api-url": Flags.string({ description: "URL da API" }),
|
|
7
|
+
instance: Flags.string({ description: "instância local configurada" }),
|
|
8
|
+
json: Flags.boolean({ description: "emite somente JSON v1 em stdout" }),
|
|
9
|
+
workspace: Flags.string({ description: "ID do workspace" }),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const listFlags = {
|
|
13
|
+
limit: Flags.integer({ min: 1, description: "quantidade por página" }),
|
|
14
|
+
page: Flags.integer({ min: 1, description: "página" }),
|
|
15
|
+
search: Flags.string({ description: "busca textual" }),
|
|
16
|
+
status: Flags.string({ description: "filtra por status" }),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class ReadCommand extends AuthenticatedApiCommand {
|
|
20
|
+
async read(flags, operation, options = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const context = await this.authenticatedContext(
|
|
23
|
+
{
|
|
24
|
+
apiUrl: flags["api-url"],
|
|
25
|
+
instance: flags.instance,
|
|
26
|
+
workspace: flags.workspace,
|
|
27
|
+
},
|
|
28
|
+
{ requireWorkspace: Boolean(options.requireWorkspace) },
|
|
29
|
+
);
|
|
30
|
+
const service = new DomainReadService(context.api);
|
|
31
|
+
const payload = await operation(service, context);
|
|
32
|
+
return {
|
|
33
|
+
context,
|
|
34
|
+
envelope: readEnvelope(
|
|
35
|
+
options.resource,
|
|
36
|
+
options.operation,
|
|
37
|
+
payload,
|
|
38
|
+
options.scope?.(context) || { workspaceId: context.workspaceId },
|
|
39
|
+
),
|
|
40
|
+
payload,
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw asCliError(error, options.resource);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
emit(flags, result, human) {
|
|
48
|
+
if (flags.json) {
|
|
49
|
+
this.output({ json: true }).result(result.envelope);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const lines = [human(result.payload)];
|
|
53
|
+
const scope = result.envelope.scope;
|
|
54
|
+
lines.push(
|
|
55
|
+
`Escopo: workspace=${scope.workspaceId || "não informado"}${scope.applicationId ? ` application=${scope.applicationId}` : ""}${scope.requestId ? ` demand=${scope.requestId}` : ""}`,
|
|
56
|
+
);
|
|
57
|
+
if (result.envelope.pagination) {
|
|
58
|
+
const page = result.envelope.pagination;
|
|
59
|
+
lines.push(
|
|
60
|
+
`Paginação: page=${page.page ?? "não informada"} limit=${page.limit ?? "não informado"} returned=${page.returned ?? "desconhecido"} total=${page.total ?? "desconhecido"} truncated=${page.truncated}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
this.output().result(lines.join("\n"));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CliError } from "../core/errors.js";
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION = "biaws.read.v1";
|
|
4
|
+
|
|
5
|
+
function queryString(parameters = {}) {
|
|
6
|
+
const query = new URLSearchParams();
|
|
7
|
+
for (const [key, value] of Object.entries(parameters)) {
|
|
8
|
+
if (value === undefined || value === null || value === "") continue;
|
|
9
|
+
query.set(key, String(value));
|
|
10
|
+
}
|
|
11
|
+
return query.size ? `?${query}` : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function pagination(payload = {}) {
|
|
15
|
+
const meta = payload.meta || {};
|
|
16
|
+
return {
|
|
17
|
+
page: meta.page ?? null,
|
|
18
|
+
limit: meta.limit ?? null,
|
|
19
|
+
total: meta.total ?? meta.count ?? null,
|
|
20
|
+
returned: Array.isArray(payload.items) ? payload.items.length : null,
|
|
21
|
+
truncated: Boolean(meta.truncated),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readEnvelope(resource, operation, payload, scope = {}) {
|
|
26
|
+
const data = operation === "list" ? payload.items || [] : payload;
|
|
27
|
+
return {
|
|
28
|
+
schemaVersion: SCHEMA_VERSION,
|
|
29
|
+
resource,
|
|
30
|
+
operation,
|
|
31
|
+
scope: {
|
|
32
|
+
workspaceId: scope.workspaceId || null,
|
|
33
|
+
applicationId: scope.applicationId || null,
|
|
34
|
+
requestId: scope.requestId || null,
|
|
35
|
+
},
|
|
36
|
+
pagination: operation === "list" ? pagination(payload) : null,
|
|
37
|
+
data,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function table(items, columns) {
|
|
42
|
+
if (!items.length) return "Nenhum resultado.";
|
|
43
|
+
const rows = [columns.map(([label]) => label)];
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
rows.push(columns.map(([, read]) => String(read(item) ?? "")));
|
|
46
|
+
}
|
|
47
|
+
const widths = rows[0].map((_, index) =>
|
|
48
|
+
Math.max(...rows.map((row) => row[index].length)),
|
|
49
|
+
);
|
|
50
|
+
return rows
|
|
51
|
+
.map((row) =>
|
|
52
|
+
row
|
|
53
|
+
.map((cell, index) => cell.padEnd(widths[index]))
|
|
54
|
+
.join(" ")
|
|
55
|
+
.trimEnd(),
|
|
56
|
+
)
|
|
57
|
+
.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function asCliError(error, resource) {
|
|
61
|
+
if (error instanceof CliError) return error;
|
|
62
|
+
const status = error?.statusCode;
|
|
63
|
+
const exitCode =
|
|
64
|
+
status === 404 ? 4 : status === 403 ? 3 : status === 401 ? 2 : 1;
|
|
65
|
+
const code =
|
|
66
|
+
status === 404
|
|
67
|
+
? "RESOURCE_NOT_FOUND"
|
|
68
|
+
: status === 403
|
|
69
|
+
? "PERMISSION_DENIED"
|
|
70
|
+
: error?.code || "API_READ_FAILED";
|
|
71
|
+
return new CliError(`${resource}: ${error.message}`, {
|
|
72
|
+
cause: error,
|
|
73
|
+
code,
|
|
74
|
+
exitCode,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class DomainReadService {
|
|
79
|
+
constructor(api) {
|
|
80
|
+
this.api = api;
|
|
81
|
+
}
|
|
82
|
+
workspaces() {
|
|
83
|
+
return this.api.request("/catalog/workspaces");
|
|
84
|
+
}
|
|
85
|
+
workspace(id) {
|
|
86
|
+
return this.api.request(`/catalog/workspaces/${encodeURIComponent(id)}`);
|
|
87
|
+
}
|
|
88
|
+
applications(workspaceId, filters) {
|
|
89
|
+
return this.api.request(
|
|
90
|
+
`/catalog/workspaces/${encodeURIComponent(workspaceId)}/applications${queryString(filters)}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
application(id) {
|
|
94
|
+
return this.api.request(`/catalog/applications/${encodeURIComponent(id)}`);
|
|
95
|
+
}
|
|
96
|
+
demands(filters) {
|
|
97
|
+
return this.api.request(`/requests${queryString(filters)}`);
|
|
98
|
+
}
|
|
99
|
+
async demand(id, filters = {}) {
|
|
100
|
+
try {
|
|
101
|
+
return await this.api.request(
|
|
102
|
+
`/requests/${encodeURIComponent(id)}${queryString(filters)}`,
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error?.statusCode !== 404) throw error;
|
|
106
|
+
const payload = await this.demands({ ...filters, code: id, limit: 2 });
|
|
107
|
+
const matches = (payload.items || []).filter(
|
|
108
|
+
(item) => String(item.clientCode || "") === String(id),
|
|
109
|
+
);
|
|
110
|
+
if (matches.length === 1) return { request: matches[0] };
|
|
111
|
+
if (matches.length > 1) {
|
|
112
|
+
throw new CliError(`Código de melhoria ambíguo: ${id}.`, {
|
|
113
|
+
code: "AMBIGUOUS_DEMAND_CODE",
|
|
114
|
+
exitCode: 2,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
issues(filters) {
|
|
121
|
+
return this.api.request(`/issues${queryString(filters)}`);
|
|
122
|
+
}
|
|
123
|
+
issue(id) {
|
|
124
|
+
return this.api.request(`/issues/${encodeURIComponent(id)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
import { AuthenticatedApiCommand } from "../baseCommands.js";
|
|
3
|
+
import { CliError } from "../core/errors.js";
|
|
4
|
+
import {
|
|
5
|
+
asWriteCliError,
|
|
6
|
+
DomainWriteService,
|
|
7
|
+
writeEnvelope,
|
|
8
|
+
} from "./writeService.js";
|
|
9
|
+
|
|
10
|
+
export const writeFlags = {
|
|
11
|
+
"api-url": Flags.string({ description: "URL da API" }),
|
|
12
|
+
instance: Flags.string({ description: "instância local configurada" }),
|
|
13
|
+
json: Flags.boolean({ description: "emite somente JSON v1 em stdout" }),
|
|
14
|
+
workspace: Flags.string({ description: "ID do workspace" }),
|
|
15
|
+
yes: Flags.boolean({ char: "y", description: "confirma a alteração" }),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class WriteCommand extends AuthenticatedApiCommand {
|
|
19
|
+
async write(flags, resource, resolve, mutate) {
|
|
20
|
+
try {
|
|
21
|
+
const context = await this.authenticatedContext(
|
|
22
|
+
{
|
|
23
|
+
apiUrl: flags["api-url"],
|
|
24
|
+
instance: flags.instance,
|
|
25
|
+
workspace: flags.workspace,
|
|
26
|
+
},
|
|
27
|
+
{ requireWorkspace: true },
|
|
28
|
+
);
|
|
29
|
+
const service = new DomainWriteService(context.api);
|
|
30
|
+
const plan = await resolve(service, context);
|
|
31
|
+
if (plan.currentStatus === plan.status) {
|
|
32
|
+
return this.emit(flags, {
|
|
33
|
+
...plan,
|
|
34
|
+
workspaceId: context.workspaceId,
|
|
35
|
+
changed: false,
|
|
36
|
+
previousStatus: plan.currentStatus,
|
|
37
|
+
data: plan.entity,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
await this.confirm(flags, plan);
|
|
41
|
+
const payload = await mutate(service, plan, context);
|
|
42
|
+
return this.emit(flags, {
|
|
43
|
+
...plan,
|
|
44
|
+
workspaceId: context.workspaceId,
|
|
45
|
+
changed: true,
|
|
46
|
+
previousStatus: plan.currentStatus,
|
|
47
|
+
data: payload,
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw asWriteCliError(error, resource);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async confirm(flags, plan) {
|
|
55
|
+
if (flags.yes) return;
|
|
56
|
+
if (!this.adapters.terminal.isInteractive) {
|
|
57
|
+
throw new CliError(
|
|
58
|
+
"Confirmação necessária. Use --yes em modo não interativo.",
|
|
59
|
+
{
|
|
60
|
+
code: "CONFIRMATION_REQUIRED",
|
|
61
|
+
exitCode: 2,
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const confirmed = await this.adapters.prompts.ask({
|
|
66
|
+
name: "confirmation",
|
|
67
|
+
type: "confirm",
|
|
68
|
+
default: false,
|
|
69
|
+
message: `${plan.label}: alterar status de ${plan.currentStatus} para ${plan.status}?`,
|
|
70
|
+
});
|
|
71
|
+
if (!confirmed) {
|
|
72
|
+
throw new CliError("Alteração cancelada.", {
|
|
73
|
+
code: "OPERATION_CANCELLED",
|
|
74
|
+
exitCode: 2,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
emit(flags, result) {
|
|
80
|
+
const envelope = writeEnvelope(result.resource, result.operation, result);
|
|
81
|
+
if (flags.json) this.output({ json: true }).result(envelope);
|
|
82
|
+
else {
|
|
83
|
+
this.output().result(
|
|
84
|
+
result.changed
|
|
85
|
+
? `${result.label}: ${result.previousStatus} -> ${result.status}`
|
|
86
|
+
: `${result.label}: já está em ${result.status}; nenhuma alteração realizada.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return envelope;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CliError } from "../core/errors.js";
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION = "biaws.write.v1";
|
|
4
|
+
|
|
5
|
+
function referenceOf(item) {
|
|
6
|
+
return String(item?.id || item?._id || item?.code || "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function findTask(payload, reference) {
|
|
10
|
+
const request = payload.request || payload;
|
|
11
|
+
const matches = (request.tasks || []).filter(
|
|
12
|
+
(task) =>
|
|
13
|
+
referenceOf(task) === String(reference) ||
|
|
14
|
+
String(task.code || "") === String(reference),
|
|
15
|
+
);
|
|
16
|
+
if (matches.length === 0) {
|
|
17
|
+
throw new CliError(`Tarefa ${reference} não encontrada na melhoria.`, {
|
|
18
|
+
code: "TASK_NOT_FOUND",
|
|
19
|
+
exitCode: 4,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (matches.length > 1) {
|
|
23
|
+
throw new CliError(
|
|
24
|
+
`A referência ${reference} identifica mais de uma tarefa.`,
|
|
25
|
+
{
|
|
26
|
+
code: "AMBIGUOUS_TASK_REFERENCE",
|
|
27
|
+
exitCode: 2,
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return { request, task: matches[0] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function writeEnvelope(resource, operation, result) {
|
|
35
|
+
return {
|
|
36
|
+
schemaVersion: SCHEMA_VERSION,
|
|
37
|
+
resource,
|
|
38
|
+
operation,
|
|
39
|
+
scope: {
|
|
40
|
+
workspaceId: result.workspaceId || null,
|
|
41
|
+
applicationId: result.applicationId || null,
|
|
42
|
+
requestId: result.requestId || null,
|
|
43
|
+
},
|
|
44
|
+
changed: result.changed,
|
|
45
|
+
previousStatus: result.previousStatus,
|
|
46
|
+
status: result.status,
|
|
47
|
+
data: result.data,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function asWriteCliError(error, resource) {
|
|
52
|
+
if (error instanceof CliError) return error;
|
|
53
|
+
const status = error?.statusCode;
|
|
54
|
+
const exitCode =
|
|
55
|
+
status === 404
|
|
56
|
+
? 4
|
|
57
|
+
: status === 403
|
|
58
|
+
? 3
|
|
59
|
+
: status === 401 || status === 422 || status === 409
|
|
60
|
+
? 2
|
|
61
|
+
: 1;
|
|
62
|
+
const fallback =
|
|
63
|
+
status === 404
|
|
64
|
+
? "RESOURCE_NOT_FOUND"
|
|
65
|
+
: status === 403
|
|
66
|
+
? "PERMISSION_DENIED"
|
|
67
|
+
: status === 409
|
|
68
|
+
? "WRITE_CONFLICT"
|
|
69
|
+
: status === 422
|
|
70
|
+
? "INVALID_TRANSITION"
|
|
71
|
+
: "API_WRITE_FAILED";
|
|
72
|
+
return new CliError(`${resource}: ${error.message}`, {
|
|
73
|
+
cause: error,
|
|
74
|
+
code: error?.code || fallback,
|
|
75
|
+
exitCode,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class DomainWriteService {
|
|
80
|
+
constructor(api) {
|
|
81
|
+
this.api = api;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async demand(id) {
|
|
85
|
+
try {
|
|
86
|
+
return await this.api.request(`/requests/${encodeURIComponent(id)}`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error?.statusCode !== 404) throw error;
|
|
89
|
+
const query = new URLSearchParams({ code: String(id), limit: "2" });
|
|
90
|
+
const payload = await this.api.request(`/requests?${query}`);
|
|
91
|
+
const matches = (payload.items || []).filter(
|
|
92
|
+
(item) => String(item.clientCode || "") === String(id),
|
|
93
|
+
);
|
|
94
|
+
if (matches.length === 1) return { request: matches[0] };
|
|
95
|
+
if (matches.length > 1) {
|
|
96
|
+
throw new CliError(`Código de melhoria ambíguo: ${id}.`, {
|
|
97
|
+
code: "AMBIGUOUS_DEMAND_CODE",
|
|
98
|
+
exitCode: 2,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
issue(id) {
|
|
106
|
+
return this.api.request(`/issues/${encodeURIComponent(id)}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateTaskStatus(demandId, taskId, status) {
|
|
110
|
+
return this.api.request(
|
|
111
|
+
`/requests/${encodeURIComponent(demandId)}/tasks/${encodeURIComponent(taskId)}`,
|
|
112
|
+
{ method: "PUT", body: JSON.stringify({ status }) },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
updateIssueStatus(issueId, status) {
|
|
117
|
+
return this.api.request(`/issues/${encodeURIComponent(issueId)}`, {
|
|
118
|
+
method: "PATCH",
|
|
119
|
+
body: JSON.stringify({ status }),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|