Skip to content

Commit f72f77b

Browse files
committed
fix: handle CTRL+C during spinners
1 parent c1cfa96 commit f72f77b

File tree

3 files changed

+35
-20
lines changed

3 files changed

+35
-20
lines changed

cli/src/commands/demo/command.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import pc from 'picocolors';
2-
import ora from 'ora';
32
import { program } from 'commander';
43
import type { FederatedGraph, Subgraph, WhoAmIResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
54
import { config } from '../../core/config.js';
@@ -24,6 +23,7 @@ import {
2423
resetScreen,
2524
runRouterContainer,
2625
updateScreenWithUserInfo,
26+
demoSpinner,
2727
} from './util.js';
2828

2929
function printHello() {
@@ -54,7 +54,7 @@ async function handleGetFederatedGraphResponse(
5454
});
5555
}
5656

57-
const spinner = ora().start();
57+
const spinner = demoSpinner().start();
5858
const getFederatedGraphResponse = await fetchFederatedGraphByName(client, {
5959
name: config.demoGraphName,
6060
namespace: config.demoNamespace,
@@ -99,7 +99,7 @@ async function cleanupFederatedGraph(
9999
cleanupFederatedGraph(client, { graphData, userInfo });
100100
}
101101

102-
const spinner = ora().start(`Removing federated graph ${pc.bold(graphData.graph.name)}…`);
102+
const spinner = demoSpinner(`Removing federated graph ${pc.bold(graphData.graph.name)}…`).start();
103103
const deleteResponse = await cleanUpFederatedGraph(client, graphData);
104104

105105
if (deleteResponse.error) {
@@ -139,7 +139,7 @@ async function handleCreateFederatedGraphResponse(
139139
const routingUrl = new URL('graphql', 'http://localhost');
140140
routingUrl.port = String(config.demoRouterPort);
141141

142-
const federatedGraphSpinner = ora().start();
142+
const federatedGraphSpinner = demoSpinner().start();
143143
const createGraphResponse = await createFederatedGraph(client, {
144144
name: config.demoGraphName,
145145
namespace: config.demoNamespace,
@@ -192,11 +192,14 @@ async function handleStep2(
192192
const graph = graphData?.graph;
193193
const subgraphs = graphData?.subgraphs ?? [];
194194
if (graph) {
195-
const cleanupFn = async () =>
195+
let deleted = false;
196+
const cleanupFn = async () => {
196197
await cleanupFederatedGraph(opts.client, {
197198
graphData: { graph, subgraphs },
198199
userInfo,
199200
});
201+
deleted = true;
202+
};
200203
await waitForKeyPress(
201204
{
202205
Enter: () => undefined,
@@ -205,6 +208,10 @@ async function handleStep2(
205208
},
206209
'Hit [ENTER] to continue or [d] to delete the federated graph and its subgraphs to start over. CTRL+C to quit.',
207210
);
211+
if (deleted) {
212+
console.log(pc.yellow('\nPlease restart the demo command to continue.\n'));
213+
process.exit(0);
214+
}
208215
return { routingUrl: graph.routingURL };
209216
}
210217

@@ -272,7 +279,7 @@ async function handleStep3(
272279
return;
273280
}
274281

275-
const spinner = ora().start('Generating router token…');
282+
const spinner = demoSpinner('Generating router token…').start();
276283
const createResult = await createRouterToken(tokenParams);
277284

278285
if (createResult.error) {
@@ -282,14 +289,15 @@ async function handleStep3(
282289
}
283290

284291
spinner.succeed('Router token generated.');
292+
console.log(` ${pc.bold(createResult.token)}`);
285293

286294
const sampleQuery = JSON.stringify({
287295
query: `query GetProductWithReviews($id: ID!) { product(id: $id) { id title price { currency amount } reviews { id author rating contents } } }`,
288296
variables: { id: 'product-1' },
289297
});
290298

291299
async function fireSampleQuery() {
292-
const querySpinner = ora('Sending sample query…').start();
300+
const querySpinner = demoSpinner('Sending sample query…').start();
293301
try {
294302
const res = await fetch(`${routerBaseUrl}/graphql`, {
295303
method: 'POST',
@@ -363,7 +371,7 @@ async function handleStep1(opts: BaseCommandOptions, userInfo: UserInfo) {
363371
}
364372

365373
async function getUserInfo(client: BaseCommandOptions['client']) {
366-
const spinner = ora('Retrieving information about you…').start();
374+
const spinner = demoSpinner('Retrieving information about you…').start();
367375
const { userInfo, error } = await fetchUserInfo(client);
368376

369377
if (error) {
@@ -381,9 +389,6 @@ async function getUserInfo(client: BaseCommandOptions['client']) {
381389
export default function (opts: BaseCommandOptions) {
382390
return async function handleCommand() {
383391
const controller = new AbortController();
384-
const cleanup = () => controller.abort();
385-
process.on('SIGINT', cleanup);
386-
process.on('SIGTERM', cleanup);
387392

388393
try {
389394
clearScreen();
@@ -425,8 +430,7 @@ export default function (opts: BaseCommandOptions) {
425430
const routerBaseUrl = new URL(step2Result.routingUrl).origin;
426431
await handleStep3(opts, { userInfo, routerBaseUrl, signal: controller.signal, logPath });
427432
} finally {
428-
process.off('SIGINT', cleanup);
429-
process.off('SIGTERM', cleanup);
433+
// no-op
430434
}
431435
};
432436
}

cli/src/commands/demo/util.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import type { BaseCommandOptions } from '../../core/types/types.js';
1212
import { visibleLength } from '../../utils.js';
1313
import type { UserInfo } from './types.js';
1414

15+
// TODO: ora defaults discardStdin to true which puts stdin into raw mode
16+
// and restores it to cooked mode when the spinner stops. This conflicts
17+
// with the demo command's own stdin management (enableRawModeWithCtrlC)
18+
// causing CTRL+C to stop working between prompts.
19+
export function demoSpinner(text?: string) {
20+
return ora({ text, discardStdin: false });
21+
}
22+
1523
/**
1624
* Clears whole screen
1725
*/
@@ -119,7 +127,7 @@ const GitHubTreeSchema = z.object({
119127
* @returns [directory] path which contains the support data
120128
*/
121129
export async function prepareSupportingData() {
122-
const spinner = ora('Preparing supporting data…').start();
130+
const spinner = demoSpinner('Preparing supporting data…').start();
123131

124132
const cosmoDir = path.join(cacheDir, 'demo');
125133
await fs.mkdir(cosmoDir, { recursive: true });
@@ -215,7 +223,7 @@ async function createDockerContainerBuilder(builderName: string): Promise<void>
215223
* properly. In case of failures, show prompt to install/setup.
216224
*/
217225
export async function checkDockerReadiness(): Promise<void> {
218-
const spinner = ora('Checking Docker availability…').start();
226+
const spinner = demoSpinner('Checking Docker availability…').start();
219227

220228
if (!(await isDockerAvailable())) {
221229
spinner.fail('Docker is not available.');
@@ -386,7 +394,7 @@ export async function runRouterContainer({
386394
args.push(config.demoRouterImage);
387395

388396
const logStream = createWriteStream(logPath, { flags: 'a' });
389-
const spinner = ora(`Starting router on ${pc.bold(routerBaseUrl)}…`).start();
397+
const spinner = demoSpinner(`Starting router on ${pc.bold(routerBaseUrl)}…`).start();
390398

391399
try {
392400
const proc = execa('docker', args, {
@@ -451,7 +459,7 @@ export async function publishAllPlugins({
451459
const pluginName = pluginNames[i];
452460
const pluginDir = path.join(supportDir, 'plugins', pluginName);
453461

454-
const spinner = ora(`Publishing plugin ${pc.bold(pluginName)} (${i + 1}/${pluginNames.length})…`).start();
462+
const spinner = demoSpinner(`Publishing plugin ${pc.bold(pluginName)} (${i + 1}/${pluginNames.length})…`).start();
455463

456464
const files = await readPluginFiles(pluginDir);
457465
const result = await publishPluginPipeline({

cli/src/utils.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,10 @@ type PrintTruncationWarningParams = {
308308
* For non-Enter keys, the callback fires immediately on keypress (no Enter required).
309309
* Ctrl+C always exits the process.
310310
*/
311-
export function waitForKeyPress(keyMap: Record<string, (() => void) | undefined>, message?: string): Promise<void> {
311+
export function waitForKeyPress(
312+
keyMap: Record<string, (() => unknown | Promise<unknown>) | undefined>,
313+
message?: string,
314+
): Promise<void> {
312315
const { promise, resolve } = Promise.withResolvers<void>();
313316

314317
if (message) {
@@ -318,7 +321,7 @@ export function waitForKeyPress(keyMap: Record<string, (() => void) | undefined>
318321
process.stdin.setRawMode(true);
319322
process.stdin.resume();
320323

321-
const onData = (data: Buffer) => {
324+
const onData = async (data: Buffer) => {
322325
const key = data.toString();
323326

324327
// Ctrl+C
@@ -337,7 +340,7 @@ export function waitForKeyPress(keyMap: Record<string, (() => void) | undefined>
337340
process.stdin.setRawMode(false);
338341
process.stdin.pause();
339342
process.stdout.write('\n');
340-
keyMap[normalized]?.();
343+
await keyMap[normalized]?.();
341344
resolve();
342345
}
343346
};

0 commit comments

Comments
 (0)