@mondaydotcomorg/z2h-cli 0.24.0 → 0.24.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/dist/commands/__tests__/generate-z2h-token.test.d.ts +2 -0
- package/dist/commands/__tests__/generate-z2h-token.test.d.ts.map +1 -0
- package/dist/commands/generate-z2h-token.d.ts.map +1 -1
- package/dist/commands/generate-z2h-token.js +17 -4
- package/dist/commands/tag.d.ts.map +1 -1
- package/dist/commands/tag.js +3 -0
- package/dist/esm/commands/__tests__/generate-z2h-token.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/generate-z2h-token.test.d.ts.map +1 -0
- package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -1
- package/dist/esm/commands/generate-z2h-token.mjs +17 -4
- package/dist/esm/commands/tag.d.ts.map +1 -1
- package/dist/esm/commands/tag.mjs +3 -0
- package/package.json +1 -1
- package/src/commands/__tests__/generate-z2h-token.test.ts +140 -0
- package/src/commands/generate-z2h-token.ts +19 -3
- package/src/commands/tag.ts +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.test.d.ts","sourceRoot":"","sources":["../../../src/commands/__tests__/generate-z2h-token.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAwED,wBAAsB,uBAAuB,CAAC,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B/F"}
|
|
@@ -15,6 +15,9 @@ const DEFAULT_TOKEN_PAGE_URL = 'https://bigbrain.me/bigbrain-vibe/generate-z2h-t
|
|
|
15
15
|
const LOOPBACK_TIMEOUT_MS = 120_000;
|
|
16
16
|
async function promptForToken() {
|
|
17
17
|
const rl = readline__default.default.createInterface({ input: node_process.stdin, output: node_process.stdout });
|
|
18
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
19
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
20
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
18
21
|
try {
|
|
19
22
|
const raw = await rl.question('Paste your token here, then press Enter:\n> ');
|
|
20
23
|
return raw.trim();
|
|
@@ -51,13 +54,23 @@ async function runLoopbackFlow(pageUrl) {
|
|
|
51
54
|
util_logger.warn('could not auto-open the browser. please visit the URL above manually.');
|
|
52
55
|
}
|
|
53
56
|
util_logger.info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
|
|
57
|
+
// A SIGINT listener suppresses Node's default termination while it is attached. That is
|
|
58
|
+
// what turns the Ctrl+C offered above into the manual-paste fallback instead of a killed
|
|
59
|
+
// CLI. The finally below detaches it, so it covers only this wait.
|
|
60
|
+
let stopListening = () => { };
|
|
61
|
+
const cancelled = new Promise(resolve => {
|
|
62
|
+
const onSigint = () => resolve(null);
|
|
63
|
+
process.once('SIGINT', onSigint);
|
|
64
|
+
stopListening = () => process.removeListener('SIGINT', onSigint);
|
|
65
|
+
});
|
|
66
|
+
// `close()` below rejects a still-pending wait. Swallow it here so the loser of the race
|
|
67
|
+
// never becomes an unhandled rejection, which Node turns into a crash.
|
|
68
|
+
const arrived = loopback.waitForToken(LOOPBACK_TIMEOUT_MS).catch(() => null);
|
|
54
69
|
try {
|
|
55
|
-
return await
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return null;
|
|
70
|
+
return await Promise.race([arrived, cancelled]);
|
|
59
71
|
}
|
|
60
72
|
finally {
|
|
73
|
+
stopListening();
|
|
61
74
|
loopback.close();
|
|
62
75
|
}
|
|
63
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6ClH;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5F"}
|
package/dist/commands/tag.js
CHANGED
|
@@ -46,6 +46,9 @@ async function resolveAppSlug(appArg) {
|
|
|
46
46
|
*/
|
|
47
47
|
async function promptForTags(appSlug, catalog, current) {
|
|
48
48
|
const rl = readline__default.default.createInterface({ input: node_process.stdin, output: node_process.stdout });
|
|
49
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
50
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
51
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
49
52
|
const selected = [];
|
|
50
53
|
try {
|
|
51
54
|
util_logger.info(`Tagging "${appSlug}".`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.test.d.ts","sourceRoot":"","sources":["../../../../src/commands/__tests__/generate-z2h-token.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;
|
|
1
|
+
{"version":3,"file":"generate-z2h-token.d.ts","sourceRoot":"","sources":["../../../src/commands/generate-z2h-token.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,sBAAsB,yDAAyD,CAAC;AAC7F,eAAO,MAAM,mBAAmB,SAAU,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAwED,wBAAsB,uBAAuB,CAAC,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B/F"}
|
|
@@ -9,6 +9,9 @@ const DEFAULT_TOKEN_PAGE_URL = 'https://bigbrain.me/bigbrain-vibe/generate-z2h-t
|
|
|
9
9
|
const LOOPBACK_TIMEOUT_MS = 120_000;
|
|
10
10
|
async function promptForToken() {
|
|
11
11
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
12
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
13
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
14
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
12
15
|
try {
|
|
13
16
|
const raw = await rl.question('Paste your token here, then press Enter:\n> ');
|
|
14
17
|
return raw.trim();
|
|
@@ -45,13 +48,23 @@ async function runLoopbackFlow(pageUrl) {
|
|
|
45
48
|
warn('could not auto-open the browser. please visit the URL above manually.');
|
|
46
49
|
}
|
|
47
50
|
info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
|
|
51
|
+
// A SIGINT listener suppresses Node's default termination while it is attached. That is
|
|
52
|
+
// what turns the Ctrl+C offered above into the manual-paste fallback instead of a killed
|
|
53
|
+
// CLI. The finally below detaches it, so it covers only this wait.
|
|
54
|
+
let stopListening = () => { };
|
|
55
|
+
const cancelled = new Promise(resolve => {
|
|
56
|
+
const onSigint = () => resolve(null);
|
|
57
|
+
process.once('SIGINT', onSigint);
|
|
58
|
+
stopListening = () => process.removeListener('SIGINT', onSigint);
|
|
59
|
+
});
|
|
60
|
+
// `close()` below rejects a still-pending wait. Swallow it here so the loser of the race
|
|
61
|
+
// never becomes an unhandled rejection, which Node turns into a crash.
|
|
62
|
+
const arrived = loopback.waitForToken(LOOPBACK_TIMEOUT_MS).catch(() => null);
|
|
48
63
|
try {
|
|
49
|
-
return await
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
64
|
+
return await Promise.race([arrived, cancelled]);
|
|
53
65
|
}
|
|
54
66
|
finally {
|
|
67
|
+
stopListening();
|
|
55
68
|
loopback.close();
|
|
56
69
|
}
|
|
57
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"tag.d.ts","sourceRoot":"","sources":["../../../src/commands/tag.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAgCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6ClH;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B5F"}
|
|
@@ -40,6 +40,9 @@ async function resolveAppSlug(appArg) {
|
|
|
40
40
|
*/
|
|
41
41
|
async function promptForTags(appSlug, catalog, current) {
|
|
42
42
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
43
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
44
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
45
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
43
46
|
const selected = [];
|
|
44
47
|
try {
|
|
45
48
|
info(`Tagging "${appSlug}".`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { generateZ2HTokenCommand } from '../generate-z2h-token';
|
|
5
|
+
import { startLoopbackServer } from '../../util/auth/loopback-server';
|
|
6
|
+
import { writeZthAuth } from '../../util/auth/write-auth';
|
|
7
|
+
import { openInBrowser } from '../../util/open-browser';
|
|
8
|
+
import { setOutputMode } from '../../util/logger';
|
|
9
|
+
|
|
10
|
+
vi.mock('node:readline/promises', () => ({
|
|
11
|
+
default: { createInterface: vi.fn() },
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('../../util/auth/loopback-server', () => ({ startLoopbackServer: vi.fn() }));
|
|
14
|
+
vi.mock('../../util/auth/write-auth', async importOriginal => ({
|
|
15
|
+
...(await importOriginal<typeof import('../../util/auth/write-auth')>()),
|
|
16
|
+
writeZthAuth: vi.fn().mockResolvedValue({ filePath: '/tmp/auth.json' }),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('../../util/open-browser', () => ({ openInBrowser: vi.fn().mockResolvedValue(undefined) }));
|
|
19
|
+
|
|
20
|
+
const PASTED_TOKEN = 'aaa.bbb.ccc';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mirrors the real LoopbackHandle contract: `close()` rejects a wait that never
|
|
24
|
+
* received a token. A race that leaves that rejection unhandled crashes the process
|
|
25
|
+
* under Node's default unhandled-rejection policy, so the fake must reproduce it.
|
|
26
|
+
*/
|
|
27
|
+
function fakeLoopback() {
|
|
28
|
+
let rejectPending: ((err: Error) => void) | null = null;
|
|
29
|
+
const close = vi.fn(() => {
|
|
30
|
+
rejectPending?.(new Error('Loopback server closed before token arrived'));
|
|
31
|
+
rejectPending = null;
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
callbackUrl: 'http://127.0.0.1:1234/token',
|
|
35
|
+
waitForToken: vi.fn(
|
|
36
|
+
() =>
|
|
37
|
+
new Promise<string>((_res, rej) => {
|
|
38
|
+
rejectPending = rej;
|
|
39
|
+
})
|
|
40
|
+
),
|
|
41
|
+
close,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('generate-z2h-token: Ctrl+C falls back to manual paste', () => {
|
|
46
|
+
let question: ReturnType<typeof vi.fn>;
|
|
47
|
+
let rlClose: ReturnType<typeof vi.fn>;
|
|
48
|
+
let rlOn: ReturnType<typeof vi.fn>;
|
|
49
|
+
const unhandled: unknown[] = [];
|
|
50
|
+
const onUnhandled = (err: unknown) => unhandled.push(err);
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
unhandled.length = 0;
|
|
55
|
+
process.on('unhandledRejection', onUnhandled);
|
|
56
|
+
setOutputMode('json');
|
|
57
|
+
question = vi.fn().mockResolvedValue(PASTED_TOKEN);
|
|
58
|
+
rlClose = vi.fn();
|
|
59
|
+
rlOn = vi.fn();
|
|
60
|
+
vi.mocked(readline.createInterface).mockReturnValue({
|
|
61
|
+
question,
|
|
62
|
+
close: rlClose,
|
|
63
|
+
on: rlOn,
|
|
64
|
+
} as unknown as ReturnType<typeof readline.createInterface>);
|
|
65
|
+
vi.mocked(startLoopbackServer).mockResolvedValue(fakeLoopback());
|
|
66
|
+
vi.mocked(writeZthAuth).mockResolvedValue({ filePath: '/tmp/auth.json' });
|
|
67
|
+
vi.mocked(openInBrowser).mockResolvedValue(undefined);
|
|
68
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
process.removeListener('unhandledRejection', onUnhandled);
|
|
73
|
+
setOutputMode('human');
|
|
74
|
+
vi.mocked(process.stdout.write).mockRestore();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('cancels the loopback wait on SIGINT and saves the pasted token instead', async () => {
|
|
78
|
+
const done = generateZ2HTokenCommand();
|
|
79
|
+
// Let runLoopbackFlow reach its wait and attach the listener before interrupting.
|
|
80
|
+
await vi.waitFor(() => expect(process.listenerCount('SIGINT')).toBeGreaterThan(0));
|
|
81
|
+
process.emit('SIGINT');
|
|
82
|
+
|
|
83
|
+
await done;
|
|
84
|
+
|
|
85
|
+
expect(question).toHaveBeenCalledOnce();
|
|
86
|
+
expect(writeZthAuth).toHaveBeenCalledWith({ token: PASTED_TOKEN });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('detaches its SIGINT listener so a later Ctrl+C is not swallowed', async () => {
|
|
90
|
+
const before = process.listenerCount('SIGINT');
|
|
91
|
+
const done = generateZ2HTokenCommand();
|
|
92
|
+
await vi.waitFor(() => expect(process.listenerCount('SIGINT')).toBe(before + 1));
|
|
93
|
+
process.emit('SIGINT');
|
|
94
|
+
await done;
|
|
95
|
+
|
|
96
|
+
expect(process.listenerCount('SIGINT')).toBe(before);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('never leaves the losing race branch as an unhandled rejection', async () => {
|
|
100
|
+
const done = generateZ2HTokenCommand();
|
|
101
|
+
await vi.waitFor(() => expect(process.listenerCount('SIGINT')).toBeGreaterThan(0));
|
|
102
|
+
process.emit('SIGINT');
|
|
103
|
+
await done;
|
|
104
|
+
// close() rejected the pending wait; that rejection must already be swallowed.
|
|
105
|
+
await new Promise(r => setImmediate(r));
|
|
106
|
+
|
|
107
|
+
expect(unhandled).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('still uses the loopback token when one arrives before any Ctrl+C', async () => {
|
|
111
|
+
const handle = fakeLoopback();
|
|
112
|
+
handle.waitForToken.mockResolvedValue('xxx.yyy.zzz');
|
|
113
|
+
vi.mocked(startLoopbackServer).mockResolvedValue(handle);
|
|
114
|
+
|
|
115
|
+
await generateZ2HTokenCommand();
|
|
116
|
+
|
|
117
|
+
expect(question).not.toHaveBeenCalled();
|
|
118
|
+
expect(writeZthAuth).toHaveBeenCalledWith({ token: 'xxx.yyy.zzz' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('--no-browser skips the loopback entirely and prompts straight away', async () => {
|
|
122
|
+
await generateZ2HTokenCommand({ noBrowser: true });
|
|
123
|
+
|
|
124
|
+
expect(startLoopbackServer).not.toHaveBeenCalled();
|
|
125
|
+
expect(openInBrowser).not.toHaveBeenCalled();
|
|
126
|
+
expect(question).toHaveBeenCalledOnce();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Raw mode swallows Ctrl-C unless the interface itself listens, so the prompt must
|
|
130
|
+
// register a handler or the CLI hangs there with no way out.
|
|
131
|
+
it('makes Ctrl-C exit the paste prompt instead of pausing it', async () => {
|
|
132
|
+
await generateZ2HTokenCommand({ noBrowser: true });
|
|
133
|
+
|
|
134
|
+
expect(rlOn).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
135
|
+
const exit = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
|
|
136
|
+
rlOn.mock.calls.find(([event]) => event === 'SIGINT')?.[1]();
|
|
137
|
+
expect(exit).toHaveBeenCalledWith(130);
|
|
138
|
+
exit.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -22,6 +22,9 @@ export interface GenerateZ2HTokenOptions {
|
|
|
22
22
|
|
|
23
23
|
async function promptForToken(): Promise<string> {
|
|
24
24
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
25
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
26
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
27
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
25
28
|
try {
|
|
26
29
|
const raw = await rl.question('Paste your token here, then press Enter:\n> ');
|
|
27
30
|
return raw.trim();
|
|
@@ -65,11 +68,24 @@ async function runLoopbackFlow(pageUrl: string): Promise<string | null> {
|
|
|
65
68
|
|
|
66
69
|
info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
|
|
67
70
|
|
|
71
|
+
// A SIGINT listener suppresses Node's default termination while it is attached. That is
|
|
72
|
+
// what turns the Ctrl+C offered above into the manual-paste fallback instead of a killed
|
|
73
|
+
// CLI. The finally below detaches it, so it covers only this wait.
|
|
74
|
+
let stopListening = (): void => {};
|
|
75
|
+
const cancelled = new Promise<null>(resolve => {
|
|
76
|
+
const onSigint = (): void => resolve(null);
|
|
77
|
+
process.once('SIGINT', onSigint);
|
|
78
|
+
stopListening = () => process.removeListener('SIGINT', onSigint);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// `close()` below rejects a still-pending wait. Swallow it here so the loser of the race
|
|
82
|
+
// never becomes an unhandled rejection, which Node turns into a crash.
|
|
83
|
+
const arrived = loopback.waitForToken(LOOPBACK_TIMEOUT_MS).catch(() => null);
|
|
84
|
+
|
|
68
85
|
try {
|
|
69
|
-
return await
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
86
|
+
return await Promise.race([arrived, cancelled]);
|
|
72
87
|
} finally {
|
|
88
|
+
stopListening();
|
|
73
89
|
loopback.close();
|
|
74
90
|
}
|
|
75
91
|
}
|
package/src/commands/tag.ts
CHANGED
|
@@ -56,6 +56,9 @@ async function resolveAppSlug(appArg?: string): Promise<string> {
|
|
|
56
56
|
*/
|
|
57
57
|
export async function promptForTags(appSlug: string, catalog: CatalogEntry[], current: AppTag[]): Promise<string[]> {
|
|
58
58
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
59
|
+
// Raw mode disables ISIG, so Ctrl-C never becomes a signal here. readline delivers the
|
|
60
|
+
// ^C byte as this event, and with no listener it only pauses. Exit code 130 is 128+SIGINT.
|
|
61
|
+
rl.on('SIGINT', () => process.exit(130));
|
|
59
62
|
const selected: string[] = [];
|
|
60
63
|
try {
|
|
61
64
|
info(`Tagging "${appSlug}".`);
|