@pagopa/dx-cli 0.22.3 → 0.22.4
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/dist/adapters/commander/presenters/__tests__/index.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/index.test.js +23 -0
- package/dist/adapters/commander/presenters/__tests__/json.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/json.test.js +108 -0
- package/dist/adapters/commander/presenters/__tests__/text.test.d.ts +1 -0
- package/dist/adapters/commander/presenters/__tests__/text.test.js +60 -0
- package/dist/adapters/commander/presenters/index.d.ts +27 -0
- package/dist/adapters/commander/presenters/index.js +16 -0
- package/dist/adapters/commander/presenters/json-command-presenter.d.ts +19 -0
- package/dist/adapters/commander/presenters/json-command-presenter.js +26 -0
- package/dist/adapters/commander/presenters/text-command-presenter.d.ts +6 -0
- package/dist/adapters/commander/presenters/text-command-presenter.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for isNonInteractive and createCommandPresenter factory.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { createCommandPresenter, isNonInteractive } from "../index.js";
|
|
6
|
+
import { JsonCommandPresenter } from "../json-command-presenter.js";
|
|
7
|
+
import { TextCommandPresenter } from "../text-command-presenter.js";
|
|
8
|
+
describe("isNonInteractive", () => {
|
|
9
|
+
it("returns true when CI is true", () => {
|
|
10
|
+
expect(isNonInteractive({ CI: true })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it("returns false when CI is false", () => {
|
|
13
|
+
expect(isNonInteractive({ CI: false })).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe("createCommandPresenter", () => {
|
|
17
|
+
it("returns a TextCommandPresenter when output is 'text'", () => {
|
|
18
|
+
expect(createCommandPresenter("text")).toBeInstanceOf(TextCommandPresenter);
|
|
19
|
+
});
|
|
20
|
+
it("returns a JsonCommandPresenter when output is 'json'", () => {
|
|
21
|
+
expect(createCommandPresenter("json")).toBeInstanceOf(JsonCommandPresenter);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for JsonCommandPresenter.
|
|
3
|
+
*
|
|
4
|
+
* JsonCommandPresenter uses a split-stream convention:
|
|
5
|
+
* - stdout for the final result/error envelope
|
|
6
|
+
* - stderr for step lifecycle (progress) events
|
|
7
|
+
*
|
|
8
|
+
* Tests verify the wire format, correct stream usage, and that values
|
|
9
|
+
* flow through correctly.
|
|
10
|
+
*/
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { JsonCommandPresenter } from "../json-command-presenter.js";
|
|
13
|
+
const captureStdout = () => {
|
|
14
|
+
const written = [];
|
|
15
|
+
vi.spyOn(process.stdout, "write").mockImplementation((data) => {
|
|
16
|
+
written.push(String(data));
|
|
17
|
+
return true;
|
|
18
|
+
});
|
|
19
|
+
return { written };
|
|
20
|
+
};
|
|
21
|
+
const captureStderr = () => {
|
|
22
|
+
const written = [];
|
|
23
|
+
vi.spyOn(process.stderr, "write").mockImplementation((data) => {
|
|
24
|
+
written.push(String(data));
|
|
25
|
+
return true;
|
|
26
|
+
});
|
|
27
|
+
return { written };
|
|
28
|
+
};
|
|
29
|
+
describe("JsonCommandPresenter", () => {
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
describe("trackStep", () => {
|
|
34
|
+
it("executes the task and returns its resolved value", async () => {
|
|
35
|
+
captureStderr();
|
|
36
|
+
const logger = new JsonCommandPresenter();
|
|
37
|
+
const result = await logger.trackStep("check terraform", () => Promise.resolve(42));
|
|
38
|
+
expect(result).toBe(42);
|
|
39
|
+
});
|
|
40
|
+
it("emits start then success events to stderr", async () => {
|
|
41
|
+
const stderr = captureStderr();
|
|
42
|
+
const logger = new JsonCommandPresenter();
|
|
43
|
+
await logger.trackStep("check terraform", () => Promise.resolve("done"));
|
|
44
|
+
expect(stderr.written).toHaveLength(2);
|
|
45
|
+
expect(JSON.parse(stderr.written[0])).toMatchObject({
|
|
46
|
+
name: "check terraform",
|
|
47
|
+
status: "start",
|
|
48
|
+
type: "step",
|
|
49
|
+
});
|
|
50
|
+
expect(JSON.parse(stderr.written[1])).toMatchObject({
|
|
51
|
+
name: "check terraform",
|
|
52
|
+
status: "success",
|
|
53
|
+
type: "step",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it("emits error event to stderr and rethrows on task failure", async () => {
|
|
57
|
+
const stderr = captureStderr();
|
|
58
|
+
const logger = new JsonCommandPresenter();
|
|
59
|
+
const error = new Error("terraform not found");
|
|
60
|
+
await expect(logger.trackStep("check terraform", () => Promise.reject(error))).rejects.toThrow("terraform not found");
|
|
61
|
+
expect(JSON.parse(stderr.written[1])).toMatchObject({
|
|
62
|
+
error: "terraform not found",
|
|
63
|
+
name: "check terraform",
|
|
64
|
+
status: "error",
|
|
65
|
+
type: "step",
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it("can run multiple sequential steps", async () => {
|
|
69
|
+
captureStderr();
|
|
70
|
+
const logger = new JsonCommandPresenter();
|
|
71
|
+
const order = [];
|
|
72
|
+
await logger.trackStep("step A", async () => {
|
|
73
|
+
order.push("A");
|
|
74
|
+
});
|
|
75
|
+
await logger.trackStep("step B", async () => {
|
|
76
|
+
order.push("B");
|
|
77
|
+
});
|
|
78
|
+
expect(order).toEqual(["A", "B"]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("reportResult", () => {
|
|
82
|
+
it("emits an ok:true JSON envelope to stdout", () => {
|
|
83
|
+
const stdout = captureStdout();
|
|
84
|
+
const logger = new JsonCommandPresenter();
|
|
85
|
+
logger.reportResult({ repository: { name: "my-repo" } });
|
|
86
|
+
expect(JSON.parse(stdout.written[0])).toEqual({
|
|
87
|
+
data: { repository: { name: "my-repo" } },
|
|
88
|
+
ok: true,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("reportError", () => {
|
|
93
|
+
it("emits an ok:false JSON envelope to stdout", () => {
|
|
94
|
+
const stdout = captureStdout();
|
|
95
|
+
const logger = new JsonCommandPresenter();
|
|
96
|
+
logger.reportError(new Error("something failed"));
|
|
97
|
+
expect(JSON.parse(stdout.written[0])).toMatchObject({
|
|
98
|
+
error: "something failed",
|
|
99
|
+
ok: false,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it("handles non-Error values without throwing", () => {
|
|
103
|
+
captureStdout();
|
|
104
|
+
const logger = new JsonCommandPresenter();
|
|
105
|
+
expect(() => logger.reportError("plain string error")).not.toThrow();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for TextCommandPresenter.
|
|
3
|
+
*
|
|
4
|
+
* The TextCommandPresenter wraps oraPromise for step feedback and chalk for
|
|
5
|
+
* final output. Tests verify the observable behavior: tasks are executed,
|
|
6
|
+
* values flow through, and errors surface correctly.
|
|
7
|
+
*/
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
// Mock oraPromise so tests don't spin up a real TTY spinner
|
|
10
|
+
vi.mock("ora", () => ({
|
|
11
|
+
oraPromise: (_promise) => _promise,
|
|
12
|
+
}));
|
|
13
|
+
import { TextCommandPresenter } from "../text-command-presenter.js";
|
|
14
|
+
describe("TextCommandPresenter", () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
describe("trackStep", () => {
|
|
19
|
+
it("executes the task and returns its resolved value", async () => {
|
|
20
|
+
const logger = new TextCommandPresenter();
|
|
21
|
+
const result = await logger.trackStep("check terraform", () => Promise.resolve(42));
|
|
22
|
+
expect(result).toBe(42);
|
|
23
|
+
});
|
|
24
|
+
it("propagates task rejection as a thrown error", async () => {
|
|
25
|
+
const logger = new TextCommandPresenter();
|
|
26
|
+
const error = new Error("terraform not found");
|
|
27
|
+
await expect(logger.trackStep("check terraform", () => Promise.reject(error))).rejects.toThrow("terraform not found");
|
|
28
|
+
});
|
|
29
|
+
it("can run multiple sequential steps", async () => {
|
|
30
|
+
const logger = new TextCommandPresenter();
|
|
31
|
+
const order = [];
|
|
32
|
+
await logger.trackStep("step A", async () => {
|
|
33
|
+
order.push("A");
|
|
34
|
+
});
|
|
35
|
+
await logger.trackStep("step B", async () => {
|
|
36
|
+
order.push("B");
|
|
37
|
+
});
|
|
38
|
+
expect(order).toEqual(["A", "B"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("reportResult", () => {
|
|
42
|
+
it("calls console.log without throwing", () => {
|
|
43
|
+
vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
44
|
+
const logger = new TextCommandPresenter();
|
|
45
|
+
expect(() => logger.reportResult({ repository: { name: "my-repo" } })).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("reportError", () => {
|
|
49
|
+
it("calls console.error without throwing", () => {
|
|
50
|
+
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
51
|
+
const logger = new TextCommandPresenter();
|
|
52
|
+
expect(() => logger.reportError(new Error("something failed"))).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
it("handles non-Error values without throwing", () => {
|
|
55
|
+
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
56
|
+
const logger = new TextCommandPresenter();
|
|
57
|
+
expect(() => logger.reportError("plain string error")).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CommandPresenter } from "../../../domain/command-presenter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Output logger factory.
|
|
4
|
+
*
|
|
5
|
+
* isNonInteractive determines whether the CLI should suppress interactive
|
|
6
|
+
* prompts (e.g. when running in a CI pipeline). This is independent of the
|
|
7
|
+
* output format: a user can request JSON output while still answering prompts,
|
|
8
|
+
* and a CI system can set CI=true with text output.
|
|
9
|
+
*
|
|
10
|
+
* createCommandPresenter selects the appropriate CommandPresenter adapter based solely
|
|
11
|
+
* on the requested output format.
|
|
12
|
+
*/
|
|
13
|
+
import type { CliEnv } from "../env.js";
|
|
14
|
+
/**
|
|
15
|
+
* Returns true when the CLI should suppress interactive prompts.
|
|
16
|
+
*
|
|
17
|
+
* Checks the parsed CI boolean from CliEnv (coerced by zod's stringbool),
|
|
18
|
+
* following the same convention used by `is-interactive` and `ora`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const isNonInteractive: (env: CliEnv) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Returns the appropriate CommandPresenter adapter for the requested output mode.
|
|
23
|
+
*
|
|
24
|
+
* - "text" → TextCommandPresenter (chalk + ora, for human terminal sessions)
|
|
25
|
+
* - "json" → JsonCommandPresenter (NDJSON on stderr, JSON envelope on stdout)
|
|
26
|
+
*/
|
|
27
|
+
export declare const createCommandPresenter: (output: "json" | "text") => CommandPresenter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JsonCommandPresenter } from "./json-command-presenter.js";
|
|
2
|
+
import { TextCommandPresenter } from "./text-command-presenter.js";
|
|
3
|
+
/**
|
|
4
|
+
* Returns true when the CLI should suppress interactive prompts.
|
|
5
|
+
*
|
|
6
|
+
* Checks the parsed CI boolean from CliEnv (coerced by zod's stringbool),
|
|
7
|
+
* following the same convention used by `is-interactive` and `ora`.
|
|
8
|
+
*/
|
|
9
|
+
export const isNonInteractive = (env) => env.CI;
|
|
10
|
+
/**
|
|
11
|
+
* Returns the appropriate CommandPresenter adapter for the requested output mode.
|
|
12
|
+
*
|
|
13
|
+
* - "text" → TextCommandPresenter (chalk + ora, for human terminal sessions)
|
|
14
|
+
* - "json" → JsonCommandPresenter (NDJSON on stderr, JSON envelope on stdout)
|
|
15
|
+
*/
|
|
16
|
+
export const createCommandPresenter = (output) => output === "json" ? new JsonCommandPresenter() : new TextCommandPresenter();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonCommandPresenter — structured adapter for the CommandPresenter domain port.
|
|
3
|
+
*
|
|
4
|
+
* Uses a split-stream convention for machine-readable output:
|
|
5
|
+
* - stdout: final result/error envelope (JSON, one line)
|
|
6
|
+
* - stderr: step lifecycle events (NDJSON progress stream)
|
|
7
|
+
*
|
|
8
|
+
* Each line is a self-describing JSON object discriminated by its fields:
|
|
9
|
+
*
|
|
10
|
+
* stderr: {"type":"step","status":"start"|"success"|"error","name":"...","error":"...?"}
|
|
11
|
+
* stdout: {"ok":true,"data":{...}}
|
|
12
|
+
* stdout: {"ok":false,"error":"..."}
|
|
13
|
+
*/
|
|
14
|
+
import type { CommandPresenter } from "../../../domain/command-presenter.js";
|
|
15
|
+
export declare class JsonCommandPresenter implements CommandPresenter {
|
|
16
|
+
reportError(error: unknown): void;
|
|
17
|
+
reportResult<T>(data: T): void;
|
|
18
|
+
trackStep<T>(name: string, task: () => Promise<T>): Promise<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { toErrorMessage } from "../error-reporting.js";
|
|
2
|
+
export class JsonCommandPresenter {
|
|
3
|
+
reportError(error) {
|
|
4
|
+
process.stdout.write(JSON.stringify({ error: toErrorMessage(error), ok: false }) + "\n");
|
|
5
|
+
}
|
|
6
|
+
reportResult(data) {
|
|
7
|
+
process.stdout.write(JSON.stringify({ data, ok: true }) + "\n");
|
|
8
|
+
}
|
|
9
|
+
async trackStep(name, task) {
|
|
10
|
+
process.stderr.write(JSON.stringify({ name, status: "start", type: "step" }) + "\n");
|
|
11
|
+
try {
|
|
12
|
+
const result = await task();
|
|
13
|
+
process.stderr.write(JSON.stringify({ name, status: "success", type: "step" }) + "\n");
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
process.stderr.write(JSON.stringify({
|
|
18
|
+
error: toErrorMessage(error),
|
|
19
|
+
name,
|
|
20
|
+
status: "error",
|
|
21
|
+
type: "step",
|
|
22
|
+
}) + "\n");
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CommandPresenter } from "../../../domain/command-presenter.js";
|
|
2
|
+
export declare class TextCommandPresenter implements CommandPresenter {
|
|
3
|
+
reportError(error: unknown): void;
|
|
4
|
+
reportResult<T>(data: T): void;
|
|
5
|
+
trackStep<T>(name: string, task: () => Promise<T>): Promise<T>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextCommandPresenter — human-readable adapter for the CommandPresenter domain port.
|
|
3
|
+
*
|
|
4
|
+
* Renders step progress via oraPromise for interactive terminal feedback,
|
|
5
|
+
* and writes results and errors to the console with chalk colouring.
|
|
6
|
+
* Intended for interactive terminal sessions (TTY).
|
|
7
|
+
*/
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { oraPromise } from "ora";
|
|
10
|
+
import { toErrorMessage } from "../error-reporting.js";
|
|
11
|
+
export class TextCommandPresenter {
|
|
12
|
+
reportError(error) {
|
|
13
|
+
console.error(chalk.red(toErrorMessage(error)));
|
|
14
|
+
}
|
|
15
|
+
reportResult(data) {
|
|
16
|
+
console.log(data);
|
|
17
|
+
}
|
|
18
|
+
async trackStep(name, task) {
|
|
19
|
+
return oraPromise(task(), { text: name });
|
|
20
|
+
}
|
|
21
|
+
}
|