@joshualiamzn/open-stack 0.0.1
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/bin/open-stack.mjs +3 -0
- package/package.json +22 -0
- package/src/aws.mjs +824 -0
- package/src/cli.mjs +200 -0
- package/src/commands/create.mjs +43 -0
- package/src/commands/describe.mjs +72 -0
- package/src/commands/help.mjs +19 -0
- package/src/commands/index.mjs +40 -0
- package/src/commands/list.mjs +26 -0
- package/src/commands/update.mjs +309 -0
- package/src/config.mjs +45 -0
- package/src/interactive.mjs +421 -0
- package/src/main.mjs +223 -0
- package/src/render.mjs +185 -0
- package/src/repl.mjs +91 -0
- package/src/ui.mjs +298 -0
package/src/render.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders the OSI pipeline YAML configuration from the resolved config.
|
|
3
|
+
*
|
|
4
|
+
* Serverless: uses template_type + template_content for all sinks
|
|
5
|
+
* Managed: uses index_type for logs, traces, and service-map
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Index template content for serverless ────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const LOGS_TEMPLATE_CONTENT = '{"version":1,"template":{"mappings":{"date_detection":false,"_source":{"enabled":true},"dynamic_templates":[{"long_resource_attributes":{"mapping":{"type":"long"},"path_match":"resource.attributes.*","match_mapping_type":"long"}},{"double_resource_attributes":{"mapping":{"type":"double"},"path_match":"resource.attributes.*","match_mapping_type":"double"}},{"string_resource_attributes":{"mapping":{"type":"keyword","ignore_above":256},"path_match":"resource.attributes.*","match_mapping_type":"string"}},{"long_scope_attributes":{"mapping":{"type":"long"},"path_match":"instrumentationScope.attributes.*","match_mapping_type":"long"}},{"double_scope_attributes":{"mapping":{"type":"double"},"path_match":"instrumentationScope.attributes.*","match_mapping_type":"double"}},{"string_scope_attributes":{"mapping":{"type":"keyword","ignore_above":256},"path_match":"instrumentationScope.attributes.*","match_mapping_type":"string"}},{"long_attributes":{"mapping":{"type":"long"},"path_match":"attributes.*","match_mapping_type":"long"}},{"double_attributes":{"mapping":{"type":"double"},"path_match":"attributes.*","match_mapping_type":"double"}},{"string_attributes":{"mapping":{"type":"keyword","ignore_above":256},"path_match":"attributes.*","match_mapping_type":"string"}}],"properties":{"droppedAttributesCount":{"type":"integer"},"instrumentationScope":{"properties":{"droppedAttributesCount":{"type":"integer"},"schemaUrl":{"type":"keyword","ignore_above":256},"name":{"type":"keyword","ignore_above":128},"version":{"type":"keyword","ignore_above":64}}},"resource":{"properties":{"droppedAttributesCount":{"type":"integer"},"schemaUrl":{"type":"keyword","ignore_above":256}}},"severity":{"properties":{"number":{"type":"integer"},"text":{"type":"keyword","ignore_above":"32"}}},"body":{"type":"text"},"@timestamp":{"type":"date_nanos"},"time":{"type":"date_nanos"},"observedTime":{"type":"date_nanos"},"traceId":{"type":"keyword","ignore_above":32},"spanId":{"type":"keyword","ignore_above":16},"flags":{"type":"long"}}}}}';
|
|
11
|
+
|
|
12
|
+
const TRACES_TEMPLATE_CONTENT = '{"version":1,"template":{"mappings":{"date_detection":false,"_source":{"enabled":true},"dynamic_templates":[{"long_resource_attributes":{"mapping":{"type":"long"},"path_match":"resource.attributes.*","match_mapping_type":"long"}},{"double_resource_attributes":{"mapping":{"type":"double"},"path_match":"resource.attributes.*","match_mapping_type":"double"}},{"string_resource_attributes":{"mapping":{"type":"keyword","ignore_above":256},"path_match":"resource.attributes.*","match_mapping_type":"string"}},{"long_scope_attributes":{"mapping":{"type":"long"},"path_match":"instrumentationScope.attributes.*","match_mapping_type":"long"}},{"double_scope_attributes":{"mapping":{"type":"double"},"path_match":"instrumentationScope.attributes.*","match_mapping_type":"double"}},{"string_scope_attributes":{"mapping":{"type":"keyword","ignore_above":256},"path_match":"instrumentationScope.attributes.*","match_mapping_type":"string"}},{"long_attributes":{"mapping":{"type":"long"},"path_match":"attributes.*","match_mapping_type":"long"}},{"double_attributes":{"mapping":{"type":"double"},"path_match":"attributes.*","match_mapping_type":"double"}},{"string_attributes":{"mapping":{"type":"keyword","ignore_above":256},"path_match":"attributes.*","match_mapping_type":"string"}}],"properties":{"droppedAttributesCount":{"type":"integer"},"instrumentationScope":{"properties":{"droppedAttributesCount":{"type":"integer"},"schemaUrl":{"type":"keyword","ignore_above":256},"name":{"type":"keyword","ignore_above":128},"version":{"type":"keyword","ignore_above":64}}},"resource":{"properties":{"droppedAttributesCount":{"type":"integer"},"schemaUrl":{"type":"keyword","ignore_above":256}}},"traceId":{"type":"keyword","ignore_above":32},"spanId":{"type":"keyword","ignore_above":16},"parentSpanId":{"type":"keyword","ignore_above":16},"name":{"ignore_above":1024,"type":"keyword"},"traceState":{"ignore_above":1024,"type":"keyword"},"traceGroup":{"ignore_above":1024,"type":"keyword"},"traceGroupFields":{"properties":{"endTime":{"type":"date_nanos"},"durationInNanos":{"type":"long"},"statusCode":{"type":"integer"}}},"kind":{"type":"keyword","ignore_above":32},"serviceName":{"type":"keyword","ignore_above":256},"startTime":{"type":"date_nanos"},"endTime":{"type":"date_nanos"},"@timestamp":{"type":"date_nanos"},"time":{"type":"date_nanos"},"status":{"properties":{"code":{"type":"integer"},"message":{"type":"keyword","ignore_above":2048}}},"durationInNanos":{"type":"long"},"events":{"type":"nested","properties":{"name":{"type":"keyword","ignore_above":256},"attributes":{"type":"object"},"droppedAttributesCount":{"type":"integer"},"time":{"type":"date_nanos"}}},"droppedEventsCount":{"type":"integer"},"links":{"type":"nested","properties":{"traceId":{"type":"keyword","ignore_above":32},"spanId":{"type":"keyword","ignore_above":16},"traceState":{"ignore_above":1024,"type":"keyword"},"attributes":{"type":"object"},"droppedAttributesCount":{"type":"integer"}}},"droppedLinksCount":{"type":"integer"}}}}}';
|
|
13
|
+
|
|
14
|
+
const SERVICE_MAP_TEMPLATE_CONTENT = '{"version":0,"template":{"mappings":{"date_detection":false,"dynamic_templates":[{"long_group_by_attributes":{"path_match":"*.groupByAttributes.*","match_mapping_type":"long","mapping":{"type":"long"}}},{"double_group_by_attributes":{"path_match":"*.groupByAttributes.*","match_mapping_type":"double","mapping":{"type":"double"}}},{"string_group_by_attributes":{"path_match":"*.groupByAttributes.*","match_mapping_type":"string","mapping":{"type":"keyword"}}},{"long_operation_attributes":{"path_match":"*.attributes.*","match_mapping_type":"long","mapping":{"type":"long"}}},{"double_operation_attributes":{"path_match":"*.attributes.*","match_mapping_type":"double","mapping":{"type":"double"}}},{"string_operation_attributes":{"path_match":"*.attributes.*","match_mapping_type":"string","mapping":{"type":"keyword"}}}],"_source":{"enabled":true},"properties":{"sourceNode":{"properties":{"type":{"type":"keyword"},"keyAttributes":{"properties":{"environment":{"type":"keyword"},"name":{"type":"keyword"}}},"groupByAttributes":{"type":"object","dynamic":"true"}}},"targetNode":{"properties":{"type":{"type":"keyword"},"keyAttributes":{"properties":{"environment":{"type":"keyword"},"name":{"type":"keyword"}}},"groupByAttributes":{"type":"object","dynamic":"true"}}},"sourceOperation":{"properties":{"name":{"type":"keyword"},"attributes":{"type":"object","dynamic":"true"}}},"targetOperation":{"properties":{"name":{"type":"keyword"},"attributes":{"type":"object","dynamic":"true"}}},"nodeConnectionHash":{"type":"keyword"},"operationConnectionHash":{"type":"keyword"},"timestamp":{"type":"date","format":"strict_date_optional_time||epoch_millis"}}}}}';
|
|
15
|
+
|
|
16
|
+
// ── Sink config helpers ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function logsSinkConfig(cfg) {
|
|
19
|
+
if (cfg.serverless) {
|
|
20
|
+
return `\
|
|
21
|
+
index: 'logs-otel-v1'
|
|
22
|
+
template_type: 'index-template'
|
|
23
|
+
template_content: '${LOGS_TEMPLATE_CONTENT}'`;
|
|
24
|
+
}
|
|
25
|
+
return `\
|
|
26
|
+
index_type: log-analytics-plain`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tracesSinkConfig(cfg) {
|
|
30
|
+
if (cfg.serverless) {
|
|
31
|
+
return `\
|
|
32
|
+
index: 'otel-v1-apm-span'
|
|
33
|
+
template_type: 'index-template'
|
|
34
|
+
template_content: '${TRACES_TEMPLATE_CONTENT}'`;
|
|
35
|
+
}
|
|
36
|
+
return `\
|
|
37
|
+
index_type: trace-analytics-plain-raw`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function serviceMapSinkConfig(cfg) {
|
|
41
|
+
if (cfg.serverless) {
|
|
42
|
+
return `\
|
|
43
|
+
index: 'otel-v2-apm-service-map'
|
|
44
|
+
template_type: 'index-template'
|
|
45
|
+
template_content: '${SERVICE_MAP_TEMPLATE_CONTENT}'`;
|
|
46
|
+
}
|
|
47
|
+
return `\
|
|
48
|
+
index_type: otel-v2-apm-service-map`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Pipeline renderer ────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export function renderPipeline(cfg) {
|
|
54
|
+
const name = cfg.pipelineName;
|
|
55
|
+
const serverless = !!cfg.serverless;
|
|
56
|
+
|
|
57
|
+
return `\
|
|
58
|
+
version: '2'
|
|
59
|
+
extension:
|
|
60
|
+
osis_configuration_metadata:
|
|
61
|
+
builder_type: visual
|
|
62
|
+
|
|
63
|
+
# Main OTLP pipeline - receives all telemetry and routes by signal type
|
|
64
|
+
otlp-pipeline:
|
|
65
|
+
source:
|
|
66
|
+
otlp:
|
|
67
|
+
logs_path: '/${name}/v1/logs'
|
|
68
|
+
traces_path: '/${name}/v1/traces'
|
|
69
|
+
metrics_path: '/${name}/v1/metrics'
|
|
70
|
+
route:
|
|
71
|
+
- logs: 'getEventType() == "LOG"'
|
|
72
|
+
- traces: 'getEventType() == "TRACE"'
|
|
73
|
+
- metrics: 'getEventType() == "METRIC"'
|
|
74
|
+
processor: []
|
|
75
|
+
sink:
|
|
76
|
+
- pipeline:
|
|
77
|
+
name: otel-logs-pipeline
|
|
78
|
+
routes:
|
|
79
|
+
- logs
|
|
80
|
+
- pipeline:
|
|
81
|
+
name: otel-traces-pipeline
|
|
82
|
+
routes:
|
|
83
|
+
- traces
|
|
84
|
+
- pipeline:
|
|
85
|
+
name: otel-metrics-pipeline
|
|
86
|
+
routes:
|
|
87
|
+
- metrics
|
|
88
|
+
|
|
89
|
+
# Log processing pipeline
|
|
90
|
+
otel-logs-pipeline:
|
|
91
|
+
source:
|
|
92
|
+
pipeline:
|
|
93
|
+
name: otlp-pipeline
|
|
94
|
+
processor:
|
|
95
|
+
- copy_values:
|
|
96
|
+
entries:
|
|
97
|
+
- from_key: "time"
|
|
98
|
+
to_key: "@timestamp"
|
|
99
|
+
sink:
|
|
100
|
+
- opensearch:
|
|
101
|
+
hosts:
|
|
102
|
+
- '${cfg.opensearchEndpoint}'
|
|
103
|
+
${logsSinkConfig(cfg)}
|
|
104
|
+
aws:
|
|
105
|
+
serverless: ${serverless}
|
|
106
|
+
region: '${cfg.region}'
|
|
107
|
+
sts_role_arn: "${cfg.iamRoleArn}"
|
|
108
|
+
|
|
109
|
+
# Trace fan-out pipeline
|
|
110
|
+
otel-traces-pipeline:
|
|
111
|
+
source:
|
|
112
|
+
pipeline:
|
|
113
|
+
name: otlp-pipeline
|
|
114
|
+
processor: []
|
|
115
|
+
sink:
|
|
116
|
+
- pipeline:
|
|
117
|
+
name: traces-raw-pipeline
|
|
118
|
+
routes: []
|
|
119
|
+
- pipeline:
|
|
120
|
+
name: service-map-pipeline
|
|
121
|
+
routes: []
|
|
122
|
+
|
|
123
|
+
# Raw trace storage pipeline
|
|
124
|
+
traces-raw-pipeline:
|
|
125
|
+
source:
|
|
126
|
+
pipeline:
|
|
127
|
+
name: otel-traces-pipeline
|
|
128
|
+
processor:
|
|
129
|
+
- otel_traces:
|
|
130
|
+
sink:
|
|
131
|
+
- opensearch:
|
|
132
|
+
hosts:
|
|
133
|
+
- '${cfg.opensearchEndpoint}'
|
|
134
|
+
${tracesSinkConfig(cfg)}
|
|
135
|
+
aws:
|
|
136
|
+
serverless: ${serverless}
|
|
137
|
+
region: '${cfg.region}'
|
|
138
|
+
sts_role_arn: "${cfg.iamRoleArn}"
|
|
139
|
+
|
|
140
|
+
# Service map generation pipeline (APM)
|
|
141
|
+
service-map-pipeline:
|
|
142
|
+
source:
|
|
143
|
+
pipeline:
|
|
144
|
+
name: otel-traces-pipeline
|
|
145
|
+
processor:
|
|
146
|
+
- otel_apm_service_map:
|
|
147
|
+
db_path: /tmp/otel-apm-service-map
|
|
148
|
+
group_by_attributes:
|
|
149
|
+
- telemetry.sdk.language
|
|
150
|
+
window_duration: ${cfg.serviceMapWindow}
|
|
151
|
+
route:
|
|
152
|
+
- otel_apm_service_map_route: 'getEventType() == "SERVICE_MAP"'
|
|
153
|
+
- service_processed_metrics: 'getEventType() == "METRIC"'
|
|
154
|
+
sink:
|
|
155
|
+
- opensearch:
|
|
156
|
+
hosts:
|
|
157
|
+
- '${cfg.opensearchEndpoint}'
|
|
158
|
+
aws:
|
|
159
|
+
serverless: ${serverless}
|
|
160
|
+
region: '${cfg.region}'
|
|
161
|
+
sts_role_arn: "${cfg.iamRoleArn}"
|
|
162
|
+
routes:
|
|
163
|
+
- otel_apm_service_map_route
|
|
164
|
+
${serviceMapSinkConfig(cfg)}
|
|
165
|
+
- prometheus:
|
|
166
|
+
url: '${cfg.prometheusUrl}'
|
|
167
|
+
aws:
|
|
168
|
+
region: '${cfg.region}'
|
|
169
|
+
routes:
|
|
170
|
+
- service_processed_metrics
|
|
171
|
+
|
|
172
|
+
# Metrics processing pipeline
|
|
173
|
+
otel-metrics-pipeline:
|
|
174
|
+
source:
|
|
175
|
+
pipeline:
|
|
176
|
+
name: otlp-pipeline
|
|
177
|
+
processor:
|
|
178
|
+
- otel_metrics:
|
|
179
|
+
sink:
|
|
180
|
+
- prometheus:
|
|
181
|
+
url: '${cfg.prometheusUrl}'
|
|
182
|
+
aws:
|
|
183
|
+
region: '${cfg.region}'
|
|
184
|
+
`;
|
|
185
|
+
}
|
package/src/repl.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { select, input } from '@inquirer/prompts';
|
|
2
|
+
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
3
|
+
import {
|
|
4
|
+
printBanner, printError, printInfo,
|
|
5
|
+
printKeyHint, printPanel, theme,
|
|
6
|
+
} from './ui.mjs';
|
|
7
|
+
import { COMMANDS, COMMAND_CHOICES } from './commands/index.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize session — prompt for region and verify AWS credentials.
|
|
11
|
+
*/
|
|
12
|
+
async function initSession() {
|
|
13
|
+
const region = await input({
|
|
14
|
+
message: 'AWS region',
|
|
15
|
+
default: 'us-east-1',
|
|
16
|
+
validate: (v) => /^[a-z]{2}-[a-z]+-\d+$/.test(v) || 'Expected format: us-east-1',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
console.error();
|
|
20
|
+
const sts = new STSClient({ region });
|
|
21
|
+
let identity;
|
|
22
|
+
try {
|
|
23
|
+
identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
printError('AWS credentials are not configured or have expired');
|
|
26
|
+
printInfo(err.message);
|
|
27
|
+
printInfo('Run "aws configure" or "aws sso login" to set up credentials, then restart.');
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
printPanel('Session', [
|
|
32
|
+
['Account', identity.Account],
|
|
33
|
+
['Region', region],
|
|
34
|
+
['Identity', theme.muted(identity.Arn)],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
return { region, accountId: identity.Account };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Start the interactive REPL loop.
|
|
42
|
+
*/
|
|
43
|
+
export async function startRepl() {
|
|
44
|
+
printBanner();
|
|
45
|
+
|
|
46
|
+
let session;
|
|
47
|
+
try {
|
|
48
|
+
session = await initSession();
|
|
49
|
+
} catch {
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.error();
|
|
54
|
+
printKeyHint([['Enter', 'select'], ['Esc', 'back'], ['Ctrl+C', 'exit']]);
|
|
55
|
+
console.error();
|
|
56
|
+
|
|
57
|
+
while (true) {
|
|
58
|
+
let cmd;
|
|
59
|
+
try {
|
|
60
|
+
cmd = await select({
|
|
61
|
+
message: theme.primary(`osi-pipeline [${session.region}]`),
|
|
62
|
+
choices: COMMAND_CHOICES,
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Ctrl+C at the menu level → exit
|
|
66
|
+
if (err.name === 'ExitPromptError') break;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cmd === 'quit') break;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await COMMANDS[cmd](session);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.name === 'ExitPromptError') {
|
|
76
|
+
// Ctrl+C during a command → return to menu
|
|
77
|
+
console.error();
|
|
78
|
+
printInfo('Cancelled.');
|
|
79
|
+
console.error();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
console.error();
|
|
83
|
+
printError(err.message);
|
|
84
|
+
console.error();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.error();
|
|
89
|
+
console.error(` ${theme.muted('Goodbye.')}`);
|
|
90
|
+
console.error();
|
|
91
|
+
}
|
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
// ── Theme colors ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export const theme = {
|
|
8
|
+
primary: chalk.hex('#B07FFF'), // soft purple (like Claude)
|
|
9
|
+
primaryBold: chalk.hex('#B07FFF').bold,
|
|
10
|
+
accent: chalk.hex('#6EC8F5'), // light blue
|
|
11
|
+
accentBold: chalk.hex('#6EC8F5').bold,
|
|
12
|
+
success: chalk.hex('#6BCB77'), // soft green
|
|
13
|
+
error: chalk.hex('#FF6B6B'), // soft red
|
|
14
|
+
warn: chalk.hex('#FFD93D'), // soft yellow
|
|
15
|
+
muted: chalk.dim,
|
|
16
|
+
mutedItalic: chalk.dim.italic,
|
|
17
|
+
label: chalk.bold,
|
|
18
|
+
highlight: chalk.hex('#E0C3FF'), // light purple
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ── Symbols ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const CHECK = '\u2713';
|
|
24
|
+
export const CROSS = '\u2717';
|
|
25
|
+
export const ARROW = '\u25B6'; // ▶
|
|
26
|
+
export const STAR = '\u2605';
|
|
27
|
+
export const DOT = '\u2022';
|
|
28
|
+
export const DIAMOND = '\u25C6';
|
|
29
|
+
|
|
30
|
+
// Box-drawing characters
|
|
31
|
+
const BOX = {
|
|
32
|
+
tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F', // rounded corners
|
|
33
|
+
h: '\u2500', v: '\u2502',
|
|
34
|
+
ltee: '\u251C', rtee: '\u2524',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Box-drawing primitives ───────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Render a box with rounded corners around lines of text.
|
|
41
|
+
* @param {string[]} lines - Lines to display inside the box
|
|
42
|
+
* @param {Object} [opts]
|
|
43
|
+
* @param {number} [opts.width] - Fixed inner width (auto-detected if omitted)
|
|
44
|
+
* @param {string} [opts.color] - Chalk method to apply to border (e.g. 'dim')
|
|
45
|
+
* @param {string} [opts.title] - Optional title in the top border
|
|
46
|
+
* @param {number} [opts.padding] - Left padding inside the box (default 1)
|
|
47
|
+
*/
|
|
48
|
+
export function renderBox(lines, opts = {}) {
|
|
49
|
+
const pad = opts.padding ?? 1;
|
|
50
|
+
const sp = ' '.repeat(pad);
|
|
51
|
+
|
|
52
|
+
// Strip ANSI for width calculation
|
|
53
|
+
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
|
|
54
|
+
const innerWidth = opts.width || Math.max(...lines.map((l) => stripAnsi(l).length)) + pad * 2;
|
|
55
|
+
|
|
56
|
+
const colorFn = opts.color === 'dim' ? chalk.dim
|
|
57
|
+
: opts.color === 'primary' ? theme.primary
|
|
58
|
+
: opts.color === 'accent' ? theme.accent
|
|
59
|
+
: chalk.dim;
|
|
60
|
+
|
|
61
|
+
let topBorder;
|
|
62
|
+
if (opts.title) {
|
|
63
|
+
const titleStr = ` ${opts.title} `;
|
|
64
|
+
const titleLen = stripAnsi(titleStr).length;
|
|
65
|
+
const remaining = innerWidth - titleLen;
|
|
66
|
+
topBorder = colorFn(BOX.tl + BOX.h) + theme.primaryBold(titleStr) + colorFn(BOX.h.repeat(Math.max(0, remaining)) + BOX.tr);
|
|
67
|
+
} else {
|
|
68
|
+
topBorder = colorFn(BOX.tl + BOX.h.repeat(innerWidth) + BOX.tr);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bottomBorder = colorFn(BOX.bl + BOX.h.repeat(innerWidth) + BOX.br);
|
|
72
|
+
|
|
73
|
+
const boxLines = [topBorder];
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const visLen = stripAnsi(line).length;
|
|
76
|
+
const rightPad = Math.max(0, innerWidth - pad * 2 - visLen);
|
|
77
|
+
boxLines.push(colorFn(BOX.v) + sp + line + ' '.repeat(rightPad) + sp + colorFn(BOX.v));
|
|
78
|
+
}
|
|
79
|
+
boxLines.push(bottomBorder);
|
|
80
|
+
|
|
81
|
+
return boxLines;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Print a box to stderr.
|
|
86
|
+
*/
|
|
87
|
+
export function printBox(lines, opts = {}) {
|
|
88
|
+
const indent = opts.indent ?? 2;
|
|
89
|
+
const prefix = ' '.repeat(indent);
|
|
90
|
+
for (const l of renderBox(lines, opts)) {
|
|
91
|
+
console.error(prefix + l);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Print a key-value panel inside a box.
|
|
97
|
+
* @param {string} title - Panel title
|
|
98
|
+
* @param {Array<[string, string]>} entries - [label, value] pairs; empty label = blank line
|
|
99
|
+
*/
|
|
100
|
+
export function printPanel(title, entries) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (const [label, value] of entries) {
|
|
103
|
+
if (!label && !value) {
|
|
104
|
+
lines.push('');
|
|
105
|
+
} else if (!label) {
|
|
106
|
+
lines.push(value);
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(`${theme.muted(label)} ${value}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
printBox(lines, { title, color: 'dim', padding: 1 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Print helpers — all write to stderr so stdout stays clean for YAML ───────
|
|
115
|
+
|
|
116
|
+
export function printHeader() {
|
|
117
|
+
console.error();
|
|
118
|
+
const banner = [
|
|
119
|
+
'',
|
|
120
|
+
`${theme.primaryBold('Open Stack CLI')}`,
|
|
121
|
+
`${theme.muted('Create AWS resources and OpenSearch Ingestion pipelines')}`,
|
|
122
|
+
'',
|
|
123
|
+
];
|
|
124
|
+
printBox(banner, { color: 'primary', padding: 2 });
|
|
125
|
+
console.error();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function printStep(msg) {
|
|
129
|
+
console.error();
|
|
130
|
+
console.error(` ${theme.primary(DIAMOND)} ${theme.primaryBold(msg)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function printSubStep(msg) {
|
|
134
|
+
console.error(` ${theme.muted(DOT)} ${msg}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function printSuccess(msg) {
|
|
138
|
+
console.error(` ${theme.success(CHECK)} ${msg}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function printError(msg) {
|
|
142
|
+
console.error(` ${theme.error(CROSS)} ${msg}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function printWarning(msg) {
|
|
146
|
+
console.error(` ${theme.warn('!')} ${msg}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function printInfo(msg) {
|
|
150
|
+
console.error(` ${theme.muted(msg)}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Spinner — wraps ora for consistent styling
|
|
154
|
+
export function createSpinner(text) {
|
|
155
|
+
return ora({ text, stream: process.stderr, spinner: 'dots', color: 'magenta' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Key hints ────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
export function printKeyHint(hints) {
|
|
161
|
+
// hints: array of [key, description] e.g. [['Ctrl+C', 'cancel'], ['Enter', 'select']]
|
|
162
|
+
const parts = hints.map(([key, desc]) =>
|
|
163
|
+
`${theme.muted('[')}${theme.accent(key)}${theme.muted(']')} ${theme.muted(desc)}`
|
|
164
|
+
);
|
|
165
|
+
console.error(` ${parts.join(' ')}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Pipeline status colorizer ────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
const STATUS_COLORS = {
|
|
171
|
+
ACTIVE: theme.success,
|
|
172
|
+
CREATING: theme.warn,
|
|
173
|
+
UPDATING: theme.warn,
|
|
174
|
+
DELETING: theme.warn,
|
|
175
|
+
CREATE_FAILED: theme.error,
|
|
176
|
+
UPDATE_FAILED: theme.error,
|
|
177
|
+
STARTING: theme.accent,
|
|
178
|
+
STOPPING: theme.accent,
|
|
179
|
+
STOPPED: theme.muted,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export function colorStatus(status) {
|
|
183
|
+
const fn = STATUS_COLORS[status] || chalk.white;
|
|
184
|
+
return fn(status);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Date formatter ───────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export function formatDate(d) {
|
|
190
|
+
if (!d) return '\u2014';
|
|
191
|
+
return new Date(d).toLocaleString('en-US', {
|
|
192
|
+
month: 'short', day: 'numeric', year: 'numeric',
|
|
193
|
+
hour: '2-digit', minute: '2-digit',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── REPL banner ──────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
export function printBanner() {
|
|
200
|
+
console.error();
|
|
201
|
+
const banner = [
|
|
202
|
+
'',
|
|
203
|
+
`${theme.primaryBold('Open Stack')}`,
|
|
204
|
+
`${theme.muted('Create, list, describe, and update OpenSearch Ingestion pipelines')}`,
|
|
205
|
+
'',
|
|
206
|
+
];
|
|
207
|
+
printBox(banner, { color: 'primary', padding: 2 });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Horizontal divider ──────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
export function printDivider() {
|
|
213
|
+
console.error(` ${theme.muted(BOX.h.repeat(60))}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Escape-to-go-back prompt wrapper ────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
export const GoBack = Symbol('GoBack');
|
|
219
|
+
|
|
220
|
+
let _keypressInit = false;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Wrap an @inquirer/prompts function so that pressing Escape cancels
|
|
224
|
+
* the prompt and returns the GoBack sentinel instead of throwing.
|
|
225
|
+
*/
|
|
226
|
+
export function withEscape(promptFn) {
|
|
227
|
+
return (...args) => {
|
|
228
|
+
if (!_keypressInit && process.stdin.isTTY) {
|
|
229
|
+
readline.emitKeypressEvents(process.stdin);
|
|
230
|
+
_keypressInit = true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const promise = promptFn(...args);
|
|
234
|
+
let escaped = false;
|
|
235
|
+
|
|
236
|
+
const onKeypress = (_ch, key) => {
|
|
237
|
+
if (key?.name === 'escape') {
|
|
238
|
+
escaped = true;
|
|
239
|
+
promise.cancel();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
process.stdin.on('keypress', onKeypress);
|
|
244
|
+
|
|
245
|
+
return promise.then(
|
|
246
|
+
(val) => { process.stdin.removeListener('keypress', onKeypress); return val; },
|
|
247
|
+
(err) => {
|
|
248
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
249
|
+
if (escaped) return GoBack;
|
|
250
|
+
throw err;
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Table formatter with box borders ─────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export function printTable(headers, rows) {
|
|
259
|
+
if (rows.length === 0) return;
|
|
260
|
+
|
|
261
|
+
const stripAnsi = (s) => String(s).replace(/\x1B\[[0-9;]*m/g, '');
|
|
262
|
+
|
|
263
|
+
// Calculate column widths
|
|
264
|
+
const widths = headers.map((h, i) =>
|
|
265
|
+
Math.max(h.length, ...rows.map((r) => stripAnsi(r[i] ?? '').length))
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const totalWidth = widths.reduce((a, b) => a + b, 0) + (widths.length - 1) * 3;
|
|
269
|
+
|
|
270
|
+
// Top border
|
|
271
|
+
console.error(` ${theme.muted(BOX.tl + BOX.h.repeat(totalWidth + 2) + BOX.tr)}`);
|
|
272
|
+
|
|
273
|
+
// Header
|
|
274
|
+
const headerLine = headers
|
|
275
|
+
.map((h, i) => theme.accentBold(h.padEnd(widths[i])))
|
|
276
|
+
.join(theme.muted(' ' + BOX.v + ' '));
|
|
277
|
+
console.error(` ${theme.muted(BOX.v)} ${headerLine} ${theme.muted(BOX.v)}`);
|
|
278
|
+
|
|
279
|
+
// Separator
|
|
280
|
+
const sep = widths.map((w) => BOX.h.repeat(w)).join(BOX.h + BOX.h + BOX.h);
|
|
281
|
+
console.error(` ${theme.muted(BOX.ltee + sep + BOX.h + BOX.h + BOX.rtee)}`);
|
|
282
|
+
|
|
283
|
+
// Rows
|
|
284
|
+
for (const row of rows) {
|
|
285
|
+
const line = row
|
|
286
|
+
.map((cell, i) => {
|
|
287
|
+
const s = String(cell ?? '');
|
|
288
|
+
const pad = widths[i] - stripAnsi(s).length;
|
|
289
|
+
return s + ' '.repeat(Math.max(0, pad));
|
|
290
|
+
})
|
|
291
|
+
.join(theme.muted(' ' + BOX.v + ' '));
|
|
292
|
+
console.error(` ${theme.muted(BOX.v)} ${line} ${theme.muted(BOX.v)}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Bottom border
|
|
296
|
+
console.error(` ${theme.muted(BOX.bl + BOX.h.repeat(totalWidth + 2) + BOX.br)}`);
|
|
297
|
+
console.error();
|
|
298
|
+
}
|