@joshualiamzn/open-stack 0.0.1 → 0.0.2
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/.kiro/memory-session-id +1 -0
- package/package.json +3 -1
- package/src/aws.mjs +680 -61
- package/src/cli.mjs +57 -7
- package/src/commands/create.mjs +200 -18
- package/src/commands/demo.mjs +193 -0
- package/src/commands/describe.mjs +53 -43
- package/src/commands/help.mjs +5 -6
- package/src/commands/index.mjs +14 -17
- package/src/commands/list.mjs +14 -13
- package/src/config.mjs +11 -1
- package/src/eks.mjs +356 -0
- package/src/interactive.mjs +290 -198
- package/src/main.mjs +111 -16
- package/src/render.mjs +1 -2
- package/src/repl.mjs +19 -43
- package/src/ui.mjs +123 -12
- package/src/commands/update.mjs +0 -309
package/src/main.mjs
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
createApsWorkspace,
|
|
9
9
|
createOsiPipeline,
|
|
10
10
|
mapOsiRoleInDomain,
|
|
11
|
+
setupDashboards,
|
|
12
|
+
createDqsPrometheusRole,
|
|
13
|
+
createDirectQueryDataSource,
|
|
14
|
+
createOpenSearchApplication,
|
|
11
15
|
} from './aws.mjs';
|
|
12
16
|
import {
|
|
13
17
|
printError,
|
|
@@ -18,6 +22,52 @@ import {
|
|
|
18
22
|
theme,
|
|
19
23
|
} from './ui.mjs';
|
|
20
24
|
|
|
25
|
+
async function runDemoCommand(cfg) {
|
|
26
|
+
const { STSClient, GetCallerIdentityCommand } = await import('@aws-sdk/client-sts');
|
|
27
|
+
const { checkDemoPrerequisites, createEksCluster, installHelmChart, installOtelDemo } = await import('./eks.mjs');
|
|
28
|
+
const { getPipelineEndpoint } = await import('./aws.mjs');
|
|
29
|
+
|
|
30
|
+
if (!cfg.region) throw new Error('--region is required (or set AWS_REGION)');
|
|
31
|
+
if (!cfg.pipeline) throw new Error('--pipeline is required');
|
|
32
|
+
|
|
33
|
+
const sts = new STSClient({ region: cfg.region });
|
|
34
|
+
const identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
35
|
+
|
|
36
|
+
checkDemoPrerequisites();
|
|
37
|
+
|
|
38
|
+
const otlpEndpoint = await getPipelineEndpoint(cfg.region, cfg.pipeline);
|
|
39
|
+
if (!otlpEndpoint) throw new Error(`No OTLP endpoint found for pipeline: ${cfg.pipeline}`);
|
|
40
|
+
printSuccess(`Pipeline endpoint: ${otlpEndpoint}`);
|
|
41
|
+
|
|
42
|
+
await createEksCluster({
|
|
43
|
+
clusterName: cfg.clusterName,
|
|
44
|
+
region: cfg.region,
|
|
45
|
+
nodeCount: cfg.nodeCount,
|
|
46
|
+
instanceType: cfg.instanceType,
|
|
47
|
+
stackName: cfg.pipeline,
|
|
48
|
+
accountId: identity.Account,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.error();
|
|
52
|
+
await installHelmChart({ otlpEndpoint });
|
|
53
|
+
|
|
54
|
+
if (!cfg.skipOtelDemo) {
|
|
55
|
+
console.error();
|
|
56
|
+
await installOtelDemo({ otlpEndpoint });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.error();
|
|
60
|
+
printBox([
|
|
61
|
+
'',
|
|
62
|
+
`${theme.success.bold(`${STAR} Demo Services Deployed! ${STAR}`)}`,
|
|
63
|
+
'',
|
|
64
|
+
`${theme.label('Cluster:')} ${cfg.clusterName}`,
|
|
65
|
+
`${theme.label('Region:')} ${cfg.region}`,
|
|
66
|
+
`${theme.label('Pipeline:')} ${cfg.pipeline}`,
|
|
67
|
+
'',
|
|
68
|
+
], { color: 'primary', padding: 2 });
|
|
69
|
+
}
|
|
70
|
+
|
|
21
71
|
export async function run() {
|
|
22
72
|
try {
|
|
23
73
|
// Parse CLI or run interactive REPL
|
|
@@ -27,6 +77,12 @@ export async function run() {
|
|
|
27
77
|
return startRepl();
|
|
28
78
|
}
|
|
29
79
|
|
|
80
|
+
// Demo subcommand
|
|
81
|
+
if (cfg._command === 'demo') {
|
|
82
|
+
await runDemoCommand(cfg);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
30
86
|
// Apply simple-mode defaults for anything not explicitly set
|
|
31
87
|
if (!cfg.mode) cfg.mode = 'simple';
|
|
32
88
|
if (cfg.mode === 'simple') applySimpleDefaults(cfg);
|
|
@@ -103,6 +159,27 @@ export async function executePipeline(cfg) {
|
|
|
103
159
|
console.error();
|
|
104
160
|
}
|
|
105
161
|
|
|
162
|
+
// Extract apsWorkspaceId from prometheusUrl if not already set
|
|
163
|
+
if (!cfg.apsWorkspaceId && cfg.prometheusUrl) {
|
|
164
|
+
const m = cfg.prometheusUrl.match(/\/workspaces\/(ws-[^/]+)\//);
|
|
165
|
+
if (m) cfg.apsWorkspaceId = m[1];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create DQS Prometheus role and Direct Query data source (connects AMP to OpenSearch)
|
|
169
|
+
if (cfg.apsWorkspaceId && cfg.dqsRoleName) {
|
|
170
|
+
await createDqsPrometheusRole(cfg);
|
|
171
|
+
console.error();
|
|
172
|
+
|
|
173
|
+
await createDirectQueryDataSource(cfg);
|
|
174
|
+
console.error();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Create OpenSearch Application and associate data sources
|
|
178
|
+
if (cfg.appName) {
|
|
179
|
+
await createOpenSearchApplication(cfg);
|
|
180
|
+
console.error();
|
|
181
|
+
}
|
|
182
|
+
|
|
106
183
|
// Generate pipeline YAML
|
|
107
184
|
const pipelineYaml = renderPipeline(cfg);
|
|
108
185
|
|
|
@@ -114,31 +191,28 @@ export async function executePipeline(cfg) {
|
|
|
114
191
|
|
|
115
192
|
// Create the OSI pipeline
|
|
116
193
|
await createOsiPipeline(cfg, pipelineYaml);
|
|
194
|
+
console.error();
|
|
195
|
+
|
|
196
|
+
// Set up OpenSearch UI and create Observability workspace
|
|
197
|
+
await setupDashboards(cfg);
|
|
117
198
|
|
|
118
199
|
// ── Final summary ───────────────────────────────────────────────────
|
|
119
200
|
console.error();
|
|
201
|
+
const pad = (l) => l.padEnd(35);
|
|
120
202
|
printBox([
|
|
121
203
|
'',
|
|
122
|
-
`${theme.success.bold(`${STAR}
|
|
204
|
+
`${theme.success.bold(`${STAR} Open Stack Setup Complete! ${STAR}`)}`,
|
|
123
205
|
'',
|
|
124
|
-
`${theme.label('Pipeline:')}
|
|
125
|
-
`${theme.label('
|
|
126
|
-
`${theme.label('
|
|
127
|
-
`${theme.label('
|
|
206
|
+
`${theme.label(pad('OSI Pipeline:'))} ${cfg.ingestEndpoints?.length ? `https://${cfg.ingestEndpoints[0]}` : cfg.pipelineName}`,
|
|
207
|
+
`${theme.label(pad('OSI Pipeline Role:'))} ${cfg.iamRoleArn}`,
|
|
208
|
+
`${theme.label(pad('OpenSearch:'))} ${cfg.opensearchEndpoint}`,
|
|
209
|
+
`${theme.label(pad('OpenSearch UI:'))} ${cfg.dashboardsUrl}`,
|
|
210
|
+
`${theme.label(pad('Prometheus:'))} ${cfg.prometheusUrl}`,
|
|
211
|
+
`${theme.label(pad('Direct Query Service Datasource:'))} ${cfg.dqsDataSourceArn || 'n/a'}`,
|
|
212
|
+
`${theme.label(pad('Direct Query Service Role:'))} ${cfg.dqsRoleArn || 'n/a'}`,
|
|
128
213
|
'',
|
|
129
214
|
], { color: 'primary', padding: 2 });
|
|
130
215
|
|
|
131
|
-
console.error();
|
|
132
|
-
printBox([
|
|
133
|
-
'',
|
|
134
|
-
`${theme.muted('# Check pipeline status')}`,
|
|
135
|
-
`${theme.accentBold(`aws osis get-pipeline --pipeline-name ${cfg.pipelineName} --region ${cfg.region}`)}`,
|
|
136
|
-
'',
|
|
137
|
-
`${theme.muted('# Delete pipeline')}`,
|
|
138
|
-
`${theme.accentBold(`aws osis delete-pipeline --pipeline-name ${cfg.pipelineName} --region ${cfg.region}`)}`,
|
|
139
|
-
'',
|
|
140
|
-
], { title: 'Useful commands', color: 'dim', padding: 2 });
|
|
141
|
-
console.error();
|
|
142
216
|
}
|
|
143
217
|
|
|
144
218
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
@@ -191,6 +265,21 @@ function printSummary(cfg) {
|
|
|
191
265
|
apsEntries.push(['Workspace alias', cfg.apsWorkspaceAlias]);
|
|
192
266
|
}
|
|
193
267
|
|
|
268
|
+
// Dashboards
|
|
269
|
+
const dashEntries = [];
|
|
270
|
+
if (cfg.dashboardsAction === 'reuse') {
|
|
271
|
+
dashEntries.push(['Action', 'reuse existing']);
|
|
272
|
+
dashEntries.push(['URL', cfg.dashboardsUrl]);
|
|
273
|
+
} else {
|
|
274
|
+
dashEntries.push(['Action', 'create new Observability workspace']);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Direct Query & Application
|
|
278
|
+
const dqEntries = [];
|
|
279
|
+
if (cfg.dqsRoleName) dqEntries.push(['DQS role', cfg.dqsRoleName]);
|
|
280
|
+
if (cfg.dqsDataSourceName) dqEntries.push(['Data source name', cfg.dqsDataSourceName]);
|
|
281
|
+
if (cfg.appName) dqEntries.push(['Application name', cfg.appName]);
|
|
282
|
+
|
|
194
283
|
// Pipeline settings
|
|
195
284
|
const tuneEntries = [
|
|
196
285
|
['Min OCU', String(cfg.minOcu)],
|
|
@@ -204,12 +293,18 @@ function printSummary(cfg) {
|
|
|
204
293
|
['', theme.accentBold('OpenSearch')],
|
|
205
294
|
...osEntries,
|
|
206
295
|
['', ''],
|
|
296
|
+
['', theme.accentBold('OpenSearch UI')],
|
|
297
|
+
...dashEntries,
|
|
298
|
+
['', ''],
|
|
207
299
|
['', theme.accentBold('IAM Role')],
|
|
208
300
|
...iamEntries,
|
|
209
301
|
['', ''],
|
|
210
302
|
['', theme.accentBold('Amazon Managed Prometheus')],
|
|
211
303
|
...apsEntries,
|
|
212
304
|
['', ''],
|
|
305
|
+
['', theme.accentBold('Direct Query & Application')],
|
|
306
|
+
...dqEntries,
|
|
307
|
+
['', ''],
|
|
213
308
|
['', theme.accentBold('Pipeline Settings')],
|
|
214
309
|
...tuneEntries,
|
|
215
310
|
]);
|
package/src/render.mjs
CHANGED
package/src/repl.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
2
|
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
3
3
|
import {
|
|
4
|
-
printBanner, printError, printInfo,
|
|
5
|
-
|
|
4
|
+
printBanner, printDivider, printError, printInfo,
|
|
5
|
+
theme, GoBack, eSelect,
|
|
6
6
|
} from './ui.mjs';
|
|
7
7
|
import { COMMANDS, COMMAND_CHOICES } from './commands/index.mjs';
|
|
8
8
|
|
|
@@ -10,39 +10,31 @@ import { COMMANDS, COMMAND_CHOICES } from './commands/index.mjs';
|
|
|
10
10
|
* Initialize session — prompt for region and verify AWS credentials.
|
|
11
11
|
*/
|
|
12
12
|
async function initSession() {
|
|
13
|
-
const
|
|
13
|
+
const envRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION;
|
|
14
|
+
const region = envRegion || await input({
|
|
14
15
|
message: 'AWS region',
|
|
15
16
|
default: 'us-east-1',
|
|
16
17
|
validate: (v) => /^[a-z]{2}-[a-z]+-\d+$/.test(v) || 'Expected format: us-east-1',
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
console.error();
|
|
20
20
|
const sts = new STSClient({ region });
|
|
21
21
|
let identity;
|
|
22
22
|
try {
|
|
23
23
|
identity = await sts.send(new GetCallerIdentityCommand({}));
|
|
24
24
|
} catch (err) {
|
|
25
25
|
printError('AWS credentials are not configured or have expired');
|
|
26
|
-
printInfo(err.message);
|
|
27
26
|
printInfo('Run "aws configure" or "aws sso login" to set up credentials, then restart.');
|
|
27
|
+
printInfo('See documentation: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html#getting-started-quickstart-new-command.');
|
|
28
28
|
throw err;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
['Account', identity.Account],
|
|
33
|
-
['Region', region],
|
|
34
|
-
['Identity', theme.muted(identity.Arn)],
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
return { region, accountId: identity.Account };
|
|
31
|
+
return { region, accountId: identity.Account, arn: identity.Arn };
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
/**
|
|
41
35
|
* Start the interactive REPL loop.
|
|
42
36
|
*/
|
|
43
37
|
export async function startRepl() {
|
|
44
|
-
printBanner();
|
|
45
|
-
|
|
46
38
|
let session;
|
|
47
39
|
try {
|
|
48
40
|
session = await initSession();
|
|
@@ -50,39 +42,23 @@ export async function startRepl() {
|
|
|
50
42
|
process.exit(1);
|
|
51
43
|
}
|
|
52
44
|
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
printBanner({ account: session.accountId, region: session.region, arn: session.arn });
|
|
46
|
+
|
|
55
47
|
console.error();
|
|
56
48
|
|
|
57
49
|
while (true) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
} catch (err) {
|
|
65
|
-
// Ctrl+C at the menu level → exit
|
|
66
|
-
if (err.name === 'ExitPromptError') break;
|
|
67
|
-
throw err;
|
|
68
|
-
}
|
|
50
|
+
const cmd = await eSelect({
|
|
51
|
+
message: theme.primary(`open-stack [${session.region}]`),
|
|
52
|
+
choices: COMMAND_CHOICES,
|
|
53
|
+
clearPromptOnDone: true,
|
|
54
|
+
});
|
|
69
55
|
|
|
70
|
-
if (cmd === 'quit') break;
|
|
56
|
+
if (cmd === GoBack || cmd === 'quit') break;
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
}
|
|
58
|
+
const result = await COMMANDS[cmd](session);
|
|
59
|
+
printDivider();
|
|
60
|
+
console.error();
|
|
61
|
+
if (result === GoBack) continue;
|
|
86
62
|
}
|
|
87
63
|
|
|
88
64
|
console.error();
|
package/src/ui.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import readline from 'node:readline';
|
|
4
|
+
import { search, input, confirm } from '@inquirer/prompts';
|
|
4
5
|
|
|
5
6
|
// ── Theme colors ─────────────────────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -28,9 +29,11 @@ export const DOT = '\u2022';
|
|
|
28
29
|
export const DIAMOND = '\u25C6';
|
|
29
30
|
|
|
30
31
|
// Box-drawing characters
|
|
32
|
+
const DIVIDER = Symbol('divider');
|
|
33
|
+
|
|
31
34
|
const BOX = {
|
|
32
35
|
tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F', // rounded corners
|
|
33
|
-
h: '\u2500', v: '\u2502',
|
|
36
|
+
h: '\u2500', v: '\u2502', lT: '\u251C', rT: '\u2524',
|
|
34
37
|
ltee: '\u251C', rtee: '\u2524',
|
|
35
38
|
};
|
|
36
39
|
|
|
@@ -51,7 +54,7 @@ export function renderBox(lines, opts = {}) {
|
|
|
51
54
|
|
|
52
55
|
// Strip ANSI for width calculation
|
|
53
56
|
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;
|
|
57
|
+
const innerWidth = opts.width || Math.max(...lines.filter((l) => typeof l === 'string').map((l) => stripAnsi(l).length)) + pad * 2;
|
|
55
58
|
|
|
56
59
|
const colorFn = opts.color === 'dim' ? chalk.dim
|
|
57
60
|
: opts.color === 'primary' ? theme.primary
|
|
@@ -62,7 +65,7 @@ export function renderBox(lines, opts = {}) {
|
|
|
62
65
|
if (opts.title) {
|
|
63
66
|
const titleStr = ` ${opts.title} `;
|
|
64
67
|
const titleLen = stripAnsi(titleStr).length;
|
|
65
|
-
const remaining = innerWidth - titleLen;
|
|
68
|
+
const remaining = innerWidth - titleLen - 1;
|
|
66
69
|
topBorder = colorFn(BOX.tl + BOX.h) + theme.primaryBold(titleStr) + colorFn(BOX.h.repeat(Math.max(0, remaining)) + BOX.tr);
|
|
67
70
|
} else {
|
|
68
71
|
topBorder = colorFn(BOX.tl + BOX.h.repeat(innerWidth) + BOX.tr);
|
|
@@ -72,6 +75,10 @@ export function renderBox(lines, opts = {}) {
|
|
|
72
75
|
|
|
73
76
|
const boxLines = [topBorder];
|
|
74
77
|
for (const line of lines) {
|
|
78
|
+
if (line === DIVIDER) {
|
|
79
|
+
boxLines.push(colorFn(BOX.lT + BOX.h.repeat(innerWidth) + BOX.rT));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
75
82
|
const visLen = stripAnsi(line).length;
|
|
76
83
|
const rightPad = Math.max(0, innerWidth - pad * 2 - visLen);
|
|
77
84
|
boxLines.push(colorFn(BOX.v) + sp + line + ' '.repeat(rightPad) + sp + colorFn(BOX.v));
|
|
@@ -118,7 +125,7 @@ export function printHeader() {
|
|
|
118
125
|
const banner = [
|
|
119
126
|
'',
|
|
120
127
|
`${theme.primaryBold('Open Stack CLI')}`,
|
|
121
|
-
`${theme.muted('Create
|
|
128
|
+
`${theme.muted('Create and manage your observability stack on AWS')}`,
|
|
122
129
|
'',
|
|
123
130
|
];
|
|
124
131
|
printBox(banner, { color: 'primary', padding: 2 });
|
|
@@ -152,7 +159,7 @@ export function printInfo(msg) {
|
|
|
152
159
|
|
|
153
160
|
// Spinner — wraps ora for consistent styling
|
|
154
161
|
export function createSpinner(text) {
|
|
155
|
-
return ora({ text, stream: process.stderr, spinner: 'dots', color: 'magenta' });
|
|
162
|
+
return ora({ text, stream: process.stderr, spinner: 'dots', color: 'magenta', indent: 2 });
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
// ── Key hints ────────────────────────────────────────────────────────────────
|
|
@@ -165,6 +172,99 @@ export function printKeyHint(hints) {
|
|
|
165
172
|
console.error(` ${parts.join(' ')}`);
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Returns a keysHelpTip theme function for @inquirer/select that appends
|
|
177
|
+
* extra key hints (e.g. Esc, Ctrl+C) to the default navigation line.
|
|
178
|
+
*/
|
|
179
|
+
export function formatKeysHelpTip(extraHints) {
|
|
180
|
+
const extra = extraHints.map(([key, desc]) =>
|
|
181
|
+
`${theme.accent(key)} ${theme.muted(desc)}`
|
|
182
|
+
).join(theme.muted(' \u2022 '));
|
|
183
|
+
return (keys) => {
|
|
184
|
+
const base = keys.map(([key, action]) =>
|
|
185
|
+
`${theme.accent(key)} ${theme.muted(action)}`
|
|
186
|
+
).join(theme.muted(' \u2022 '));
|
|
187
|
+
return `${base} ${theme.muted('\u2022')} ${extra}`;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Shared escape-wrapped prompts ────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const _promptPrefix = { idle: ' ?', done: ' ✔', canceled: ' ✖' };
|
|
194
|
+
|
|
195
|
+
const _selectKeyTheme = {
|
|
196
|
+
prefix: _promptPrefix,
|
|
197
|
+
style: { keysHelpTip: formatKeysHelpTip([['Esc', 'back']]) },
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const eInput = withEscape(input);
|
|
201
|
+
export const eConfirm = withEscape(confirm);
|
|
202
|
+
|
|
203
|
+
// ── Searchable select ───────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Select prompt with built-in incremental search and Escape-to-go-back.
|
|
207
|
+
* Type to filter, arrow keys to navigate, Enter to select, Esc to go back.
|
|
208
|
+
*/
|
|
209
|
+
export function eSelect(opts) {
|
|
210
|
+
if (!_keypressInit && process.stdin.isTTY) {
|
|
211
|
+
readline.emitKeypressEvents(process.stdin);
|
|
212
|
+
_keypressInit = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const searchableChoices = (opts.choices || []).filter((c) => c.type !== 'separator' && !c.disabled);
|
|
216
|
+
|
|
217
|
+
const promise = search({
|
|
218
|
+
message: opts.message || 'Select',
|
|
219
|
+
theme: _selectKeyTheme,
|
|
220
|
+
source: (term) => {
|
|
221
|
+
if (!term) return searchableChoices;
|
|
222
|
+
const lower = term.toLowerCase();
|
|
223
|
+
return searchableChoices.filter((c) => {
|
|
224
|
+
const name = (c.name || String(c.value || '')).toLowerCase();
|
|
225
|
+
return name.includes(lower);
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Intercept tab at emit level to prevent search prompt's autocomplete
|
|
231
|
+
const origEmit = process.stdin.emit;
|
|
232
|
+
process.stdin.emit = function (event, ...args) {
|
|
233
|
+
if (event === 'keypress' && args[1]?.name === 'tab') return true;
|
|
234
|
+
return origEmit.apply(this, [event, ...args]);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
let escaped = false;
|
|
238
|
+
const onKeypress = (_ch, key) => {
|
|
239
|
+
if (key?.name === 'escape') {
|
|
240
|
+
escaped = true;
|
|
241
|
+
promise.cancel();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
process.stdin.on('keypress', onKeypress);
|
|
246
|
+
|
|
247
|
+
const cleanup = () => {
|
|
248
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
249
|
+
process.stdin.emit = origEmit;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return promise.then(
|
|
253
|
+
(val) => { cleanup(); return val; },
|
|
254
|
+
(err) => {
|
|
255
|
+
cleanup();
|
|
256
|
+
if (escaped) return GoBack;
|
|
257
|
+
if (err.name === 'ExitPromptError') {
|
|
258
|
+
console.error();
|
|
259
|
+
console.error(` ${theme.muted('Goodbye.')}`);
|
|
260
|
+
console.error();
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
168
268
|
// ── Pipeline status colorizer ────────────────────────────────────────────────
|
|
169
269
|
|
|
170
270
|
const STATUS_COLORS = {
|
|
@@ -196,15 +296,18 @@ export function formatDate(d) {
|
|
|
196
296
|
|
|
197
297
|
// ── REPL banner ──────────────────────────────────────────────────────────────
|
|
198
298
|
|
|
199
|
-
export function printBanner() {
|
|
299
|
+
export function printBanner(session) {
|
|
200
300
|
console.error();
|
|
201
|
-
const
|
|
202
|
-
''
|
|
203
|
-
`${theme.primaryBold('Open Stack')}`,
|
|
204
|
-
`${theme.muted('Create, list, describe, and update OpenSearch Ingestion pipelines')}`,
|
|
205
|
-
'',
|
|
301
|
+
const lines = [
|
|
302
|
+
`${theme.muted('Create and manage your observability stack on AWS')}`,
|
|
206
303
|
];
|
|
207
|
-
|
|
304
|
+
if (session) {
|
|
305
|
+
lines.push(DIVIDER);
|
|
306
|
+
lines.push(`${theme.muted('Account')} ${session.account}`);
|
|
307
|
+
lines.push(`${theme.muted('Region')} ${session.region}`);
|
|
308
|
+
lines.push(`${theme.muted('Identity')} ${theme.muted(session.arn)}`);
|
|
309
|
+
}
|
|
310
|
+
printBox(lines, { title: 'Open Stack', color: 'primary', padding: 2 });
|
|
208
311
|
}
|
|
209
312
|
|
|
210
313
|
// ── Horizontal divider ──────────────────────────────────────────────────────
|
|
@@ -222,6 +325,7 @@ let _keypressInit = false;
|
|
|
222
325
|
/**
|
|
223
326
|
* Wrap an @inquirer/prompts function so that pressing Escape cancels
|
|
224
327
|
* the prompt and returns the GoBack sentinel instead of throwing.
|
|
328
|
+
* @param {Function} promptFn
|
|
225
329
|
*/
|
|
226
330
|
export function withEscape(promptFn) {
|
|
227
331
|
return (...args) => {
|
|
@@ -247,6 +351,13 @@ export function withEscape(promptFn) {
|
|
|
247
351
|
(err) => {
|
|
248
352
|
process.stdin.removeListener('keypress', onKeypress);
|
|
249
353
|
if (escaped) return GoBack;
|
|
354
|
+
// Ctrl+C → exit immediately
|
|
355
|
+
if (err.name === 'ExitPromptError') {
|
|
356
|
+
console.error();
|
|
357
|
+
console.error(` ${theme.muted('Goodbye.')}`);
|
|
358
|
+
console.error();
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
250
361
|
throw err;
|
|
251
362
|
},
|
|
252
363
|
);
|