@saws/cli 2.0.0-beta.2 → 2.0.0-beta.3
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 +96 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/saws.d.ts +0 -1
- package/dist/bin/saws.js +28 -40
- package/dist/commands/deploy/command.d.ts +2 -7
- package/dist/commands/deploy/command.js +13 -19
- package/dist/commands/deploy/index.d.ts +0 -1
- package/dist/commands/deploy/index.js +1 -4
- package/dist/commands/dev/command.d.ts +1 -7
- package/dist/commands/dev/command.js +46 -48
- package/dist/commands/dev/index.d.ts +0 -1
- package/dist/commands/dev/index.js +1 -6
- package/dist/commands/dev/tui/dev-tui.d.ts +18 -3
- package/dist/commands/dev/tui/dev-tui.js +263 -29
- package/dist/commands/host/command.d.ts +2 -2
- package/dist/commands/host/command.js +130 -7
- package/dist/commands/host/index.d.ts +0 -1
- package/dist/commands/host/index.js +4 -3
- package/dist/commands/init/command.d.ts +1 -7
- package/dist/commands/init/command.js +24 -15
- package/dist/commands/init/index.d.ts +0 -1
- package/dist/commands/init/index.js +1 -5
- package/dist/commands/init/templates/gitignore.template.d.ts +1 -0
- package/dist/commands/init/templates/gitignore.template.js +4 -0
- package/dist/commands/init/templates/saws-ts.template.d.ts +3 -0
- package/dist/commands/init/templates/saws-ts.template.js +7 -0
- package/dist/commands/init/templates/tsconfig-json.template.d.ts +1 -0
- package/dist/commands/init/templates/tsconfig-json.template.js +4 -0
- package/dist/commands/secrets/command.d.ts +2 -2
- package/dist/commands/secrets/command.js +21 -9
- package/dist/commands/secrets/index.d.ts +0 -1
- package/dist/commands/secrets/index.js +7 -5
- package/dist/hosts.d.ts +1 -3
- package/dist/hosts.js +11 -10
- package/package.json +8 -25
- package/src/bin/saws.ts +59 -0
- package/src/commands/deploy/command.ts +20 -0
- package/src/commands/deploy/index.ts +8 -0
- package/src/commands/dev/command.ts +68 -0
- package/src/commands/dev/index.ts +5 -0
- package/src/commands/dev/tui/dev-tui.ts +471 -0
- package/src/commands/execute/command.ts.tmp +43 -0
- package/src/commands/execute/index.ts.tmp +9 -0
- package/src/commands/host/command.ts +188 -0
- package/src/commands/host/index.ts +18 -0
- package/src/commands/init/command.ts +28 -0
- package/src/commands/init/index.ts +8 -0
- package/src/commands/init/templates/gitignore.template.ts +4 -0
- package/src/commands/init/templates/saws-ts.template.ts +8 -0
- package/src/commands/init/templates/tsconfig-json.template.ts +4 -0
- package/src/commands/secrets/command.ts +42 -0
- package/src/commands/secrets/index.ts +13 -0
- package/src/hosts.ts +21 -0
- package/tsconfig.json +9 -0
- package/dist/bin/saws.d.ts.map +0 -1
- package/dist/commands/deploy/command.d.ts.map +0 -1
- package/dist/commands/deploy/index.d.ts.map +0 -1
- package/dist/commands/dev/command.d.ts.map +0 -1
- package/dist/commands/dev/index.d.ts.map +0 -1
- package/dist/commands/dev/tui/dev-tui.d.ts.map +0 -1
- package/dist/commands/host/command.d.ts.map +0 -1
- package/dist/commands/host/command.test.d.ts +0 -2
- package/dist/commands/host/command.test.d.ts.map +0 -1
- package/dist/commands/host/command.test.js +0 -13
- package/dist/commands/host/index.d.ts.map +0 -1
- package/dist/commands/init/command.d.ts.map +0 -1
- package/dist/commands/init/command.test.d.ts +0 -2
- package/dist/commands/init/command.test.d.ts.map +0 -1
- package/dist/commands/init/command.test.js +0 -40
- package/dist/commands/init/index.d.ts.map +0 -1
- package/dist/commands/secrets/command.d.ts.map +0 -1
- package/dist/commands/secrets/index.d.ts.map +0 -1
- package/dist/hosts.d.ts.map +0 -1
|
@@ -1,63 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createCacheDir } from "@saws/core/utils/create-directories";
|
|
2
|
+
import { onProcessExit } from "@saws/core/utils/on-exit";
|
|
3
|
+
import { getSawsConfig } from "@saws/core";
|
|
2
4
|
import { DevTui } from "./tui/dev-tui.js";
|
|
3
|
-
const
|
|
4
|
-
export async function devCommand(path, options) {
|
|
5
|
+
export const devCommand = async (path) => {
|
|
5
6
|
process.env.NODE_ENV = "development";
|
|
6
|
-
process.env.STAGE =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
7
|
+
process.env.STAGE = "local";
|
|
8
|
+
process.env.AWS_REGION = "us-west-2";
|
|
9
|
+
await createCacheDir();
|
|
10
|
+
const serviceDefinition = await getSawsConfig(path);
|
|
11
|
+
const services = collectServices(serviceDefinition);
|
|
10
12
|
const useTui = process.stdout.isTTY && process.stdin.isTTY;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const shutdown = async () => {
|
|
13
|
+
const tui = new DevTui(services.map((service) => service.name));
|
|
14
|
+
const shutdown = () => {
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
+
serviceDefinition.exit();
|
|
16
17
|
}
|
|
17
18
|
finally {
|
|
18
19
|
tui.stop();
|
|
19
|
-
process.exit();
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
process.once("SIGTERM",
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
logSink: useTui ? tui.logSink : undefined,
|
|
31
|
-
}));
|
|
32
|
-
// Service dev hooks start their processes without blocking so every
|
|
33
|
-
// dependency can come up. Keep the command alive until a signal invokes
|
|
34
|
-
// the shared shutdown path.
|
|
35
|
-
if (!options.dryRun) {
|
|
36
|
-
await new Promise(() => { });
|
|
37
|
-
}
|
|
22
|
+
onProcessExit(shutdown);
|
|
23
|
+
process.once("SIGTERM", () => {
|
|
24
|
+
shutdown();
|
|
25
|
+
process.exit();
|
|
26
|
+
});
|
|
27
|
+
if (useTui) {
|
|
28
|
+
serviceDefinition.setRuntimeLogSink(tui.logSink);
|
|
29
|
+
tui.start();
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
await serviceDefinition.dev();
|
|
32
|
+
if (useTui) {
|
|
33
|
+
for (const service of services) {
|
|
34
|
+
service.getStdOut()?.on("data", (chunk) => {
|
|
35
|
+
tui.logSink({
|
|
36
|
+
serviceName: service.name,
|
|
37
|
+
stream: "stdout",
|
|
38
|
+
chunk: chunk.toString("utf8"),
|
|
39
|
+
timestamp: new Date(),
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
service.getStdErr()?.on("data", (chunk) => {
|
|
43
|
+
tui.logSink({
|
|
44
|
+
serviceName: service.name,
|
|
45
|
+
stream: "stderr",
|
|
46
|
+
chunk: chunk.toString("utf8"),
|
|
47
|
+
timestamp: new Date(),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
42
50
|
}
|
|
43
|
-
throw error;
|
|
44
51
|
}
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
else {
|
|
53
|
+
for (const service of services) {
|
|
54
|
+
service.getStdOut()?.pipe(process.stdout);
|
|
55
|
+
service.getStdErr()?.pipe(process.stderr);
|
|
56
|
+
}
|
|
47
57
|
}
|
|
48
|
-
}
|
|
58
|
+
};
|
|
49
59
|
function collectServices(root) {
|
|
50
|
-
|
|
51
|
-
const seen = new Set();
|
|
52
|
-
const visit = (service) => {
|
|
53
|
-
if (seen.has(service))
|
|
54
|
-
return;
|
|
55
|
-
seen.add(service);
|
|
56
|
-
for (const dependency of service.dependencies) {
|
|
57
|
-
visit(dependency);
|
|
58
|
-
}
|
|
59
|
-
services.push(service.name);
|
|
60
|
-
};
|
|
61
|
-
visit(root);
|
|
62
|
-
return services;
|
|
60
|
+
return [...new Set(root.getAllDependencies())];
|
|
63
61
|
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { devCommand } from "./command.js";
|
|
3
|
-
export const createCommand = () => new Command("dev")
|
|
4
|
-
.argument("[string]", "path to service definition")
|
|
5
|
-
.option("--dry-run", "print local Docker commands instead of executing them")
|
|
6
|
-
.option("--root-dir <string>", "project root used for generated local files")
|
|
7
|
-
.option("--config <string>", "path to service definition")
|
|
8
|
-
.action(devCommand);
|
|
3
|
+
export const createCommand = () => new Command("dev").argument("[string]", "path to service definition").action(devCommand);
|
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
export interface RuntimeLogEntry {
|
|
2
|
+
serviceName: string;
|
|
3
|
+
stream: "stdout" | "stderr";
|
|
4
|
+
chunk: string;
|
|
5
|
+
timestamp: Date;
|
|
6
|
+
}
|
|
7
|
+
export type RuntimeLogSink = (entry: RuntimeLogEntry) => void;
|
|
2
8
|
export declare class DevTui {
|
|
3
9
|
readonly logSink: RuntimeLogSink;
|
|
4
10
|
private readonly services;
|
|
5
11
|
private readonly logs;
|
|
12
|
+
private readonly logScrollOffsets;
|
|
6
13
|
private selectedIndex;
|
|
7
14
|
private isStarted;
|
|
15
|
+
private isSelectionMode;
|
|
8
16
|
private previousRawMode;
|
|
9
17
|
private readonly maxLines;
|
|
10
18
|
constructor(services: string[]);
|
|
11
19
|
start(): void;
|
|
12
20
|
stop(): void;
|
|
13
|
-
writeSystemLog(message: string, stream?: "stdout" | "stderr"): void;
|
|
14
21
|
private handleKeypress;
|
|
22
|
+
private handleInput;
|
|
15
23
|
private addLog;
|
|
16
24
|
private render;
|
|
25
|
+
private enterSelectionMode;
|
|
26
|
+
private exitSelectionMode;
|
|
27
|
+
private getHelpText;
|
|
28
|
+
private getSelectedService;
|
|
29
|
+
private getBodyHeight;
|
|
30
|
+
private scrollSelectedLog;
|
|
31
|
+
private selectServiceAt;
|
|
32
|
+
private clampLogScrollOffset;
|
|
17
33
|
}
|
|
18
|
-
//# sourceMappingURL=dev-tui.d.ts.map
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
const CLEAR_SCREEN = "\x1b[2J";
|
|
3
|
+
const CLEAR_LINE = "\x1b[2K";
|
|
3
4
|
const CURSOR_HOME = "\x1b[H";
|
|
4
5
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
5
6
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
6
7
|
const ENTER_ALT_SCREEN = "\x1b[?1049h";
|
|
7
8
|
const EXIT_ALT_SCREEN = "\x1b[?1049l";
|
|
9
|
+
const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1006h";
|
|
10
|
+
const DISABLE_MOUSE = "\x1b[?1000l\x1b[?1006l";
|
|
8
11
|
const RESET = "\x1b[0m";
|
|
9
12
|
const INVERSE = "\x1b[7m";
|
|
10
13
|
const DIM = "\x1b[2m";
|
|
11
14
|
const CYAN = "\x1b[36m";
|
|
15
|
+
const WHEEL_SCROLL_LINES = 3;
|
|
12
16
|
export class DevTui {
|
|
13
17
|
logSink = (entry) => this.addLog(entry);
|
|
14
18
|
services;
|
|
15
19
|
logs = new Map();
|
|
20
|
+
logScrollOffsets = new Map();
|
|
16
21
|
selectedIndex = 0;
|
|
17
22
|
isStarted = false;
|
|
23
|
+
isSelectionMode = false;
|
|
18
24
|
previousRawMode = false;
|
|
19
25
|
maxLines = 2000;
|
|
20
26
|
constructor(services) {
|
|
21
|
-
this.services =
|
|
27
|
+
this.services = [...new Set(services)];
|
|
22
28
|
for (const service of this.services) {
|
|
23
29
|
this.logs.set(service, { lines: [], partial: "" });
|
|
30
|
+
this.logScrollOffsets.set(service, 0);
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
start() {
|
|
@@ -30,10 +37,11 @@ export class DevTui {
|
|
|
30
37
|
}
|
|
31
38
|
this.isStarted = true;
|
|
32
39
|
this.previousRawMode = process.stdin.isRaw;
|
|
33
|
-
process.stdout.write(`${ENTER_ALT_SCREEN}${HIDE_CURSOR}`);
|
|
40
|
+
process.stdout.write(`${ENTER_ALT_SCREEN}${HIDE_CURSOR}${ENABLE_MOUSE}`);
|
|
34
41
|
readline.emitKeypressEvents(process.stdin);
|
|
35
42
|
process.stdin.setRawMode(true);
|
|
36
43
|
process.stdin.resume();
|
|
44
|
+
process.stdin.on("data", this.handleInput);
|
|
37
45
|
process.stdin.on("keypress", this.handleKeypress);
|
|
38
46
|
process.stdout.on("resize", this.render);
|
|
39
47
|
this.render();
|
|
@@ -41,21 +49,29 @@ export class DevTui {
|
|
|
41
49
|
stop() {
|
|
42
50
|
if (!this.isStarted)
|
|
43
51
|
return;
|
|
52
|
+
process.stdin.off("data", this.handleInput);
|
|
44
53
|
process.stdin.off("keypress", this.handleKeypress);
|
|
45
54
|
process.stdout.off("resize", this.render);
|
|
55
|
+
this.isSelectionMode = false;
|
|
46
56
|
process.stdin.setRawMode(this.previousRawMode);
|
|
47
|
-
process.stdout.write(`${SHOW_CURSOR}${EXIT_ALT_SCREEN}${RESET}`);
|
|
57
|
+
process.stdout.write(`${DISABLE_MOUSE}${SHOW_CURSOR}${EXIT_ALT_SCREEN}${RESET}`);
|
|
48
58
|
this.isStarted = false;
|
|
49
59
|
}
|
|
50
|
-
writeSystemLog(message, stream = "stdout") {
|
|
51
|
-
this.addLog({
|
|
52
|
-
serviceName: "system",
|
|
53
|
-
stream,
|
|
54
|
-
chunk: message.endsWith("\n") ? message : `${message}\n`,
|
|
55
|
-
timestamp: new Date(),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
60
|
handleKeypress = (_input, key) => {
|
|
61
|
+
if (key.ctrl && key.name === "c") {
|
|
62
|
+
process.kill(process.pid, "SIGINT");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (this.isSelectionMode) {
|
|
66
|
+
if (key.name === "escape" || key.name === "c") {
|
|
67
|
+
this.exitSelectionMode();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.name === "c") {
|
|
72
|
+
this.enterSelectionMode();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
59
75
|
if (key.name === "up" || key.name === "k") {
|
|
60
76
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
61
77
|
this.render();
|
|
@@ -66,45 +82,86 @@ export class DevTui {
|
|
|
66
82
|
this.render();
|
|
67
83
|
return;
|
|
68
84
|
}
|
|
69
|
-
if (key.
|
|
70
|
-
|
|
85
|
+
if (key.name === "pageup" || key.name === "u") {
|
|
86
|
+
this.scrollSelectedLog(this.getBodyHeight());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.name === "pagedown" || key.name === "d") {
|
|
90
|
+
this.scrollSelectedLog(-this.getBodyHeight());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.name === "home" || key.name === "g") {
|
|
94
|
+
this.scrollSelectedLog(Number.POSITIVE_INFINITY);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (key.name === "end" || (key.shift && key.name === "g")) {
|
|
98
|
+
this.logScrollOffsets.set(this.getSelectedService(), 0);
|
|
99
|
+
this.render();
|
|
71
100
|
return;
|
|
72
101
|
}
|
|
73
102
|
if (key.name === "q") {
|
|
74
103
|
process.kill(process.pid, "SIGTERM");
|
|
75
104
|
}
|
|
76
105
|
};
|
|
106
|
+
handleInput = (chunk) => {
|
|
107
|
+
if (this.isSelectionMode)
|
|
108
|
+
return;
|
|
109
|
+
const input = chunk.toString();
|
|
110
|
+
for (const event of parseMouseEvents(input)) {
|
|
111
|
+
if (event.type === "wheel-up") {
|
|
112
|
+
this.scrollSelectedLog(WHEEL_SCROLL_LINES);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (event.type === "wheel-down") {
|
|
116
|
+
this.scrollSelectedLog(-WHEEL_SCROLL_LINES);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (event.type === "press") {
|
|
120
|
+
this.selectServiceAt(event.x, event.y);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
77
124
|
addLog(entry) {
|
|
78
|
-
|
|
125
|
+
if (entry.serviceName === "system")
|
|
126
|
+
return;
|
|
127
|
+
const serviceName = entry.serviceName;
|
|
128
|
+
if (!this.logs.has(serviceName)) {
|
|
129
|
+
this.services.push(serviceName);
|
|
130
|
+
this.logs.set(serviceName, { lines: [], partial: "" });
|
|
131
|
+
this.logScrollOffsets.set(serviceName, 0);
|
|
132
|
+
this.selectedIndex = this.services.length - 1;
|
|
133
|
+
}
|
|
79
134
|
const log = this.logs.get(serviceName) ?? { lines: [], partial: "" };
|
|
80
|
-
const pieces = `${log.partial}${entry.chunk}`.split(
|
|
135
|
+
const pieces = `${log.partial}${sanitizeLogChunk(entry.chunk)}`.split("\n");
|
|
81
136
|
log.partial = pieces.pop() ?? "";
|
|
82
137
|
for (const piece of pieces) {
|
|
83
|
-
log.lines.push(`${timestamp(entry.timestamp)} ${piece}`);
|
|
138
|
+
log.lines.push(`${timestamp(entry.timestamp)} ${entry.stream === "stderr" ? "[err] " : ""}${piece}`);
|
|
84
139
|
}
|
|
85
140
|
if (log.lines.length > this.maxLines) {
|
|
86
141
|
log.lines.splice(0, log.lines.length - this.maxLines);
|
|
87
142
|
}
|
|
88
143
|
this.logs.set(serviceName, log);
|
|
144
|
+
this.clampLogScrollOffset(serviceName);
|
|
89
145
|
this.render();
|
|
90
146
|
}
|
|
91
|
-
render = () => {
|
|
92
|
-
if (!this.isStarted || !process.stdout.isTTY)
|
|
147
|
+
render = (force = false) => {
|
|
148
|
+
if (!this.isStarted || !process.stdout.isTTY || (this.isSelectionMode && !force))
|
|
93
149
|
return;
|
|
94
150
|
const width = process.stdout.columns ?? 80;
|
|
95
|
-
const
|
|
96
|
-
const navWidth = Math.min(30, Math.max(18, Math.floor(width * 0.28)));
|
|
151
|
+
const navWidth = getNavWidth(width);
|
|
97
152
|
const logWidth = Math.max(10, width - navWidth - 1);
|
|
98
|
-
const bodyHeight =
|
|
99
|
-
const selectedService = this.
|
|
153
|
+
const bodyHeight = this.getBodyHeight();
|
|
154
|
+
const selectedService = this.getSelectedService();
|
|
100
155
|
const log = this.logs.get(selectedService) ?? { lines: [], partial: "" };
|
|
101
156
|
const visibleLines = [...log.lines, log.partial].filter(Boolean);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
const wrappedLogLines = visibleLines.flatMap((line) => wrapLine(line, logWidth));
|
|
158
|
+
const logScrollOffset = this.clampLogScrollOffset(selectedService, wrappedLogLines.length);
|
|
159
|
+
const logEnd = Math.max(bodyHeight, wrappedLogLines.length - logScrollOffset);
|
|
160
|
+
const logStart = Math.max(0, logEnd - bodyHeight);
|
|
161
|
+
const logLines = wrappedLogLines.slice(logStart, logEnd);
|
|
105
162
|
const rows = [];
|
|
106
|
-
rows.push(`${CYAN}SAWS dev${RESET}${DIM}
|
|
107
|
-
rows.push(`${"Services".padEnd(navWidth)} ${
|
|
163
|
+
rows.push(`${CYAN}SAWS dev${RESET}${DIM} ${this.getHelpText()}${RESET}`.padEnd(width));
|
|
164
|
+
rows.push(`${"Services".padEnd(navWidth)} ${logHeading(selectedService, logScrollOffset).padEnd(logWidth)}`);
|
|
108
165
|
for (let index = 0; index < bodyHeight; index += 1) {
|
|
109
166
|
const service = this.services[index] ?? "";
|
|
110
167
|
const isSelected = index === this.selectedIndex;
|
|
@@ -113,7 +170,124 @@ export class DevTui {
|
|
|
113
170
|
const logLine = truncate(logLines[index] ?? "", logWidth).padEnd(logWidth);
|
|
114
171
|
rows.push(`${isSelected ? `${INVERSE}${nav}${RESET}` : nav} ${logLine}`);
|
|
115
172
|
}
|
|
116
|
-
process.stdout.write(
|
|
173
|
+
process.stdout.write(renderRows(rows, width, process.stdout.rows ?? 24));
|
|
174
|
+
};
|
|
175
|
+
enterSelectionMode() {
|
|
176
|
+
this.isSelectionMode = true;
|
|
177
|
+
process.stdout.write(`${DISABLE_MOUSE}${SHOW_CURSOR}`);
|
|
178
|
+
this.render(true);
|
|
179
|
+
}
|
|
180
|
+
exitSelectionMode() {
|
|
181
|
+
this.isSelectionMode = false;
|
|
182
|
+
process.stdout.write(`${HIDE_CURSOR}${ENABLE_MOUSE}`);
|
|
183
|
+
this.render(true);
|
|
184
|
+
}
|
|
185
|
+
getHelpText() {
|
|
186
|
+
if (this.isSelectionMode)
|
|
187
|
+
return "select/copy text with terminal, esc resume";
|
|
188
|
+
return "up/down select, pgup/pgdn scroll, c copy/select, q quit";
|
|
189
|
+
}
|
|
190
|
+
getSelectedService() {
|
|
191
|
+
return this.services[this.selectedIndex] ?? "";
|
|
192
|
+
}
|
|
193
|
+
getBodyHeight() {
|
|
194
|
+
return Math.max(1, (process.stdout.rows ?? 24) - 3);
|
|
195
|
+
}
|
|
196
|
+
scrollSelectedLog(delta) {
|
|
197
|
+
const selectedService = this.getSelectedService();
|
|
198
|
+
const current = this.logScrollOffsets.get(selectedService) ?? 0;
|
|
199
|
+
this.logScrollOffsets.set(selectedService, current + delta);
|
|
200
|
+
this.render();
|
|
201
|
+
}
|
|
202
|
+
selectServiceAt(x, y) {
|
|
203
|
+
const navWidth = getNavWidth(process.stdout.columns ?? 80);
|
|
204
|
+
if (x > navWidth || y < 3)
|
|
205
|
+
return;
|
|
206
|
+
const serviceIndex = y - 3;
|
|
207
|
+
if (serviceIndex < 0 || serviceIndex >= this.services.length)
|
|
208
|
+
return;
|
|
209
|
+
this.selectedIndex = serviceIndex;
|
|
210
|
+
this.render();
|
|
211
|
+
}
|
|
212
|
+
clampLogScrollOffset(serviceName, wrappedLineCount) {
|
|
213
|
+
const width = process.stdout.columns ?? 80;
|
|
214
|
+
const navWidth = getNavWidth(width);
|
|
215
|
+
const logWidth = Math.max(10, width - navWidth - 1);
|
|
216
|
+
const log = this.logs.get(serviceName) ?? { lines: [], partial: "" };
|
|
217
|
+
const lineCount = wrappedLineCount ??
|
|
218
|
+
[...log.lines, log.partial].filter(Boolean).flatMap((line) => wrapLine(line, logWidth))
|
|
219
|
+
.length;
|
|
220
|
+
const maxOffset = Math.max(0, lineCount - this.getBodyHeight());
|
|
221
|
+
const nextOffset = Math.min(maxOffset, Math.max(0, this.logScrollOffsets.get(serviceName) ?? 0));
|
|
222
|
+
this.logScrollOffsets.set(serviceName, nextOffset);
|
|
223
|
+
return nextOffset;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function logHeading(serviceName, scrollOffset) {
|
|
227
|
+
if (scrollOffset === 0)
|
|
228
|
+
return `Logs: ${serviceName}`;
|
|
229
|
+
return `Logs: ${serviceName} (${scrollOffset} lines from bottom)`;
|
|
230
|
+
}
|
|
231
|
+
function renderRows(rows, width, height) {
|
|
232
|
+
const output = [CURSOR_HOME, CLEAR_SCREEN];
|
|
233
|
+
for (let index = 0; index < height; index += 1) {
|
|
234
|
+
output.push(cursorPosition(index + 1, 1));
|
|
235
|
+
output.push(CLEAR_LINE);
|
|
236
|
+
output.push(truncate(rows[index] ?? "", width).padEnd(width));
|
|
237
|
+
}
|
|
238
|
+
return output.join("");
|
|
239
|
+
}
|
|
240
|
+
function cursorPosition(row, column) {
|
|
241
|
+
return `\x1b[${row};${column}H`;
|
|
242
|
+
}
|
|
243
|
+
function getNavWidth(terminalWidth) {
|
|
244
|
+
return Math.min(30, Math.max(18, Math.floor(terminalWidth * 0.28)));
|
|
245
|
+
}
|
|
246
|
+
function parseMouseEvents(input) {
|
|
247
|
+
const events = [];
|
|
248
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
249
|
+
if (input.charCodeAt(index) !== 27 || input[index + 1] !== "[" || input[index + 2] !== "<") {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const codeResult = readUnsignedNumber(input, index + 3);
|
|
253
|
+
if (codeResult == null || input[codeResult.nextIndex] !== ";")
|
|
254
|
+
continue;
|
|
255
|
+
const xResult = readUnsignedNumber(input, codeResult.nextIndex + 1);
|
|
256
|
+
if (xResult == null || input[xResult.nextIndex] !== ";")
|
|
257
|
+
continue;
|
|
258
|
+
const yResult = readUnsignedNumber(input, xResult.nextIndex + 1);
|
|
259
|
+
if (yResult == null)
|
|
260
|
+
continue;
|
|
261
|
+
const action = input[yResult.nextIndex];
|
|
262
|
+
index = yResult.nextIndex;
|
|
263
|
+
if (action !== "M")
|
|
264
|
+
continue;
|
|
265
|
+
if (codeResult.value >= 64 && codeResult.value <= 95) {
|
|
266
|
+
const wheelDirection = codeResult.value & 3;
|
|
267
|
+
if (wheelDirection === 0) {
|
|
268
|
+
events.push({ type: "wheel-up", x: xResult.value, y: yResult.value });
|
|
269
|
+
}
|
|
270
|
+
else if (wheelDirection === 1) {
|
|
271
|
+
events.push({ type: "wheel-down", x: xResult.value, y: yResult.value });
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if ((codeResult.value & 3) === 0) {
|
|
276
|
+
events.push({ type: "press", x: xResult.value, y: yResult.value });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return events;
|
|
280
|
+
}
|
|
281
|
+
function readUnsignedNumber(value, startIndex) {
|
|
282
|
+
let index = startIndex;
|
|
283
|
+
while (index < value.length && value[index] >= "0" && value[index] <= "9") {
|
|
284
|
+
index += 1;
|
|
285
|
+
}
|
|
286
|
+
if (index === startIndex)
|
|
287
|
+
return null;
|
|
288
|
+
return {
|
|
289
|
+
value: Number(value.slice(startIndex, index)),
|
|
290
|
+
nextIndex: index,
|
|
117
291
|
};
|
|
118
292
|
}
|
|
119
293
|
function timestamp(date) {
|
|
@@ -142,5 +316,65 @@ function truncate(value, width) {
|
|
|
142
316
|
return `${value.slice(0, width - 3)}...`;
|
|
143
317
|
}
|
|
144
318
|
function visibleLength(value) {
|
|
145
|
-
return value
|
|
319
|
+
return stripAnsiColor(value).length;
|
|
320
|
+
}
|
|
321
|
+
function sanitizeLogChunk(value) {
|
|
322
|
+
return stripAnsiControlSequences(value.replaceAll("\r\n", "\n").replaceAll("\r", "\n"));
|
|
323
|
+
}
|
|
324
|
+
function stripAnsiControlSequences(value) {
|
|
325
|
+
let stripped = "";
|
|
326
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
327
|
+
const code = value.charCodeAt(index);
|
|
328
|
+
if (code === 27) {
|
|
329
|
+
index = skipEscapeSequence(value, index);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if ((code >= 0 && code < 9) || (code > 13 && code < 32) || code === 127) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
stripped += value[index];
|
|
336
|
+
}
|
|
337
|
+
return stripped;
|
|
338
|
+
}
|
|
339
|
+
function skipEscapeSequence(value, startIndex) {
|
|
340
|
+
const next = value[startIndex + 1];
|
|
341
|
+
if (next === "[") {
|
|
342
|
+
let index = startIndex + 2;
|
|
343
|
+
while (index < value.length) {
|
|
344
|
+
const code = value.charCodeAt(index);
|
|
345
|
+
if (code >= 64 && code <= 126)
|
|
346
|
+
return index;
|
|
347
|
+
index += 1;
|
|
348
|
+
}
|
|
349
|
+
return value.length - 1;
|
|
350
|
+
}
|
|
351
|
+
if (next === "]") {
|
|
352
|
+
let index = startIndex + 2;
|
|
353
|
+
while (index < value.length) {
|
|
354
|
+
if (value.charCodeAt(index) === 7)
|
|
355
|
+
return index;
|
|
356
|
+
if (value.charCodeAt(index) === 27 && value[index + 1] === "\\")
|
|
357
|
+
return index + 1;
|
|
358
|
+
index += 1;
|
|
359
|
+
}
|
|
360
|
+
return value.length - 1;
|
|
361
|
+
}
|
|
362
|
+
return Math.min(startIndex + 1, value.length - 1);
|
|
363
|
+
}
|
|
364
|
+
function stripAnsiColor(value) {
|
|
365
|
+
let stripped = "";
|
|
366
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
367
|
+
if (value.charCodeAt(index) !== 27) {
|
|
368
|
+
stripped += value[index];
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (value[index + 1] !== "[") {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
index += 2;
|
|
375
|
+
while (index < value.length && /[0-9;]/.test(value[index] ?? "")) {
|
|
376
|
+
index += 1;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return stripped;
|
|
146
380
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Host } from "@saws/core";
|
|
2
2
|
export interface ConfigureHostCommandOptions {
|
|
3
3
|
config?: string;
|
|
4
4
|
dryRun?: boolean;
|
|
5
|
+
user: string;
|
|
5
6
|
}
|
|
6
7
|
export declare function configureHostCommand(name: string | undefined, options: ConfigureHostCommandOptions): Promise<void>;
|
|
7
8
|
export declare function selectHost(hosts: Host[], name?: string): Host;
|
|
8
|
-
//# sourceMappingURL=command.d.ts.map
|