devlyn-cli 4.2.0 → 4.2.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/README.md +4 -2
- package/bin/devlyn.js +184 -147
- package/bin/skill-history.json +140 -0
- package/bin/skill-ownership.js +67 -0
- package/node_modules/@inquirer/ansi/LICENSE +22 -0
- package/node_modules/@inquirer/ansi/README.md +121 -0
- package/node_modules/@inquirer/ansi/dist/index.d.ts +14 -0
- package/node_modules/@inquirer/ansi/dist/index.js +21 -0
- package/node_modules/@inquirer/ansi/package.json +79 -0
- package/node_modules/@inquirer/checkbox/LICENSE +22 -0
- package/node_modules/@inquirer/checkbox/README.md +265 -0
- package/node_modules/@inquirer/checkbox/dist/index.d.ts +67 -0
- package/node_modules/@inquirer/checkbox/dist/index.js +210 -0
- package/node_modules/@inquirer/checkbox/package.json +93 -0
- package/node_modules/@inquirer/core/LICENSE +22 -0
- package/node_modules/@inquirer/core/README.md +424 -0
- package/node_modules/@inquirer/core/dist/index.d.ts +13 -0
- package/node_modules/@inquirer/core/dist/index.js +12 -0
- package/node_modules/@inquirer/core/dist/lib/Separator.d.ts +10 -0
- package/node_modules/@inquirer/core/dist/lib/Separator.js +21 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p1.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p1.js +4 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p2.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p2.js +3 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p3.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p3.js +5 -0
- package/node_modules/@inquirer/core/dist/lib/create-prompt.d.ts +11 -0
- package/node_modules/@inquirer/core/dist/lib/create-prompt.js +185 -0
- package/node_modules/@inquirer/core/dist/lib/errors.d.ts +20 -0
- package/node_modules/@inquirer/core/dist/lib/errors.js +21 -0
- package/node_modules/@inquirer/core/dist/lib/hook-engine.d.ts +17 -0
- package/node_modules/@inquirer/core/dist/lib/hook-engine.js +109 -0
- package/node_modules/@inquirer/core/dist/lib/key.d.ts +15 -0
- package/node_modules/@inquirer/core/dist/lib/key.js +34 -0
- package/node_modules/@inquirer/core/dist/lib/make-theme.d.ts +3 -0
- package/node_modules/@inquirer/core/dist/lib/make-theme.js +33 -0
- package/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.d.ts +15 -0
- package/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.js +121 -0
- package/node_modules/@inquirer/core/dist/lib/screen-manager.d.ts +14 -0
- package/node_modules/@inquirer/core/dist/lib/screen-manager.js +84 -0
- package/node_modules/@inquirer/core/dist/lib/theme.d.ts +166 -0
- package/node_modules/@inquirer/core/dist/lib/theme.js +29 -0
- package/node_modules/@inquirer/core/dist/lib/use-effect.d.ts +2 -0
- package/node_modules/@inquirer/core/dist/lib/use-effect.js +11 -0
- package/node_modules/@inquirer/core/dist/lib/use-keypress.d.ts +3 -0
- package/node_modules/@inquirer/core/dist/lib/use-keypress.js +20 -0
- package/node_modules/@inquirer/core/dist/lib/use-memo.d.ts +1 -0
- package/node_modules/@inquirer/core/dist/lib/use-memo.js +14 -0
- package/node_modules/@inquirer/core/dist/lib/use-prefix.d.ts +5 -0
- package/node_modules/@inquirer/core/dist/lib/use-prefix.js +35 -0
- package/node_modules/@inquirer/core/dist/lib/use-ref.d.ts +6 -0
- package/node_modules/@inquirer/core/dist/lib/use-ref.js +4 -0
- package/node_modules/@inquirer/core/dist/lib/use-state.d.ts +7 -0
- package/node_modules/@inquirer/core/dist/lib/use-state.js +28 -0
- package/node_modules/@inquirer/core/dist/lib/utils.d.ts +13 -0
- package/node_modules/@inquirer/core/dist/lib/utils.js +25 -0
- package/node_modules/@inquirer/core/package.json +98 -0
- package/node_modules/@inquirer/figures/LICENSE +22 -0
- package/node_modules/@inquirer/figures/dist/index.d.ts +275 -0
- package/node_modules/@inquirer/figures/dist/index.js +315 -0
- package/node_modules/@inquirer/figures/package.json +81 -0
- package/node_modules/@inquirer/select/LICENSE +22 -0
- package/node_modules/@inquirer/select/README.md +258 -0
- package/node_modules/@inquirer/select/dist/index.d.ts +42 -0
- package/node_modules/@inquirer/select/dist/index.js +191 -0
- package/node_modules/@inquirer/select/package.json +93 -0
- package/node_modules/@inquirer/type/LICENSE +22 -0
- package/node_modules/@inquirer/type/dist/index.d.ts +2 -0
- package/node_modules/@inquirer/type/dist/index.js +2 -0
- package/node_modules/@inquirer/type/dist/inquirer.d.ts +35 -0
- package/node_modules/@inquirer/type/dist/inquirer.js +1 -0
- package/node_modules/@inquirer/type/dist/utils.d.ts +45 -0
- package/node_modules/@inquirer/type/dist/utils.js +16 -0
- package/node_modules/@inquirer/type/package.json +88 -0
- package/node_modules/cli-width/LICENSE +13 -0
- package/node_modules/cli-width/README.md +71 -0
- package/node_modules/cli-width/index.d.ts +13 -0
- package/node_modules/cli-width/index.js +49 -0
- package/node_modules/cli-width/package.json +40 -0
- package/node_modules/fast-string-truncated-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/index.js +111 -0
- package/node_modules/fast-string-truncated-width/dist/types.d.ts +19 -0
- package/node_modules/fast-string-truncated-width/dist/types.js +2 -0
- package/node_modules/fast-string-truncated-width/dist/utils.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/utils.js +20 -0
- package/node_modules/fast-string-truncated-width/license +21 -0
- package/node_modules/fast-string-truncated-width/package.json +35 -0
- package/node_modules/fast-string-truncated-width/readme.md +59 -0
- package/node_modules/fast-string-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-width/dist/index.js +14 -0
- package/node_modules/fast-string-width/license +21 -0
- package/node_modules/fast-string-width/package.json +34 -0
- package/node_modules/fast-string-width/readme.md +42 -0
- package/node_modules/fast-wrap-ansi/LICENSE +23 -0
- package/node_modules/fast-wrap-ansi/README.md +26 -0
- package/node_modules/fast-wrap-ansi/lib/main.d.ts +6 -0
- package/node_modules/fast-wrap-ansi/lib/main.js +218 -0
- package/node_modules/fast-wrap-ansi/package.json +51 -0
- package/node_modules/mute-stream/LICENSE +15 -0
- package/node_modules/mute-stream/README.md +68 -0
- package/node_modules/mute-stream/lib/index.js +142 -0
- package/node_modules/mute-stream/package.json +54 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +74 -0
- package/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
- package/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/browser.js +10 -0
- package/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
- package/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
- package/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/index.js +279 -0
- package/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
- package/node_modules/signal-exit/dist/cjs/package.json +3 -0
- package/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
- package/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/signals.js +42 -0
- package/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
- package/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/browser.js +4 -0
- package/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
- package/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/index.js +275 -0
- package/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/package.json +3 -0
- package/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
- package/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/signals.js +39 -0
- package/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
- package/node_modules/signal-exit/package.json +106 -0
- package/package.json +16 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
3
|
+
import { withResolver } from '@inquirer/type';
|
|
4
|
+
import MuteStream from 'mute-stream';
|
|
5
|
+
import { onExit as onSignalExit } from 'signal-exit';
|
|
6
|
+
import ScreenManager from "./screen-manager.js";
|
|
7
|
+
import { withHooks, effectScheduler } from "./hook-engine.js";
|
|
8
|
+
import { AbortPromptError, CancelPromptError, ExitPromptError } from "./errors.js";
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
// Capture the real setImmediate at module load time so it works even when test
|
|
11
|
+
// frameworks mock timers with vi.useFakeTimers() or similar.
|
|
12
|
+
const nativeSetImmediate = globalThis.setImmediate;
|
|
13
|
+
/**
|
|
14
|
+
* Register an event listener and return its disposer, so registrations collapse
|
|
15
|
+
* into a single statement: `cleanups.add(listenTo(rl, 'close', handler))`.
|
|
16
|
+
*/
|
|
17
|
+
function listenTo(target, event, listener) {
|
|
18
|
+
if ('on' in target) {
|
|
19
|
+
target.on(event, listener);
|
|
20
|
+
return () => target.removeListener(event, listener);
|
|
21
|
+
}
|
|
22
|
+
target.addEventListener(event, listener);
|
|
23
|
+
return () => target.removeEventListener(event, listener);
|
|
24
|
+
}
|
|
25
|
+
function getCallSites() {
|
|
26
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
27
|
+
const savedPrepareStackTrace = Error.prepareStackTrace;
|
|
28
|
+
let result = [];
|
|
29
|
+
try {
|
|
30
|
+
Error.prepareStackTrace = (_, callSites) => {
|
|
31
|
+
const callSitesWithoutCurrent = callSites.slice(1);
|
|
32
|
+
result = callSitesWithoutCurrent;
|
|
33
|
+
return callSitesWithoutCurrent;
|
|
34
|
+
};
|
|
35
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
36
|
+
new Error().stack;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// An error will occur if the Node flag --frozen-intrinsics is used.
|
|
40
|
+
// https://nodejs.org/api/cli.html#--frozen-intrinsics
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
Error.prepareStackTrace = savedPrepareStackTrace;
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function createPrompt(view) {
|
|
47
|
+
const callSites = getCallSites();
|
|
48
|
+
const prompt = (config, context = {}) => {
|
|
49
|
+
// Default `input` to stdin
|
|
50
|
+
const { input = process.stdin, signal } = context;
|
|
51
|
+
const cleanups = new Set();
|
|
52
|
+
// Add mute capabilities to the output
|
|
53
|
+
const output = new MuteStream();
|
|
54
|
+
output.pipe(context.output ?? process.stdout);
|
|
55
|
+
/* oxlint-disable typescript/no-unsafe-type-assertion */
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
terminal: true,
|
|
58
|
+
input,
|
|
59
|
+
output,
|
|
60
|
+
});
|
|
61
|
+
/* oxlint-enable typescript/no-unsafe-type-assertion */
|
|
62
|
+
// Mute the output after readline has initialized so readline can perform
|
|
63
|
+
// any terminal setup writes (e.g. Windows Console API initialization)
|
|
64
|
+
// before suppressing output. ScreenManager will unmute/mute around each
|
|
65
|
+
// render call as needed.
|
|
66
|
+
output.mute();
|
|
67
|
+
const screen = new ScreenManager(rl);
|
|
68
|
+
const { promise, resolve, reject } = withResolver();
|
|
69
|
+
return withHooks(rl, (cycle) => {
|
|
70
|
+
// Clear hook effects synchronously before the prompt settles: readline
|
|
71
|
+
// emits keypresses synchronously within a single data event, so a
|
|
72
|
+
// microtask-deferred cleanup would let same-tick input keep driving a
|
|
73
|
+
// settled prompt.
|
|
74
|
+
const clearEffects = AsyncResource.bind(() => effectScheduler.clearAll());
|
|
75
|
+
// The promise resolvers and effectScheduler.clearAll() are both idempotent,
|
|
76
|
+
// so repeated settlement attempts are harmless no-ops. If a hook cleanup
|
|
77
|
+
// throws, the cleanup error supersedes the settlement: the answer (or
|
|
78
|
+
// settlement error, e.g. AbortPromptError) is dropped in favor of
|
|
79
|
+
// rejecting with the cleanup error.
|
|
80
|
+
const settlePrompt = (settle) => {
|
|
81
|
+
try {
|
|
82
|
+
clearEffects();
|
|
83
|
+
settle();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
reject(error);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const resolvePrompt = (value) => settlePrompt(() => resolve(value));
|
|
90
|
+
const rejectPrompt = (error) => settlePrompt(() => reject(error));
|
|
91
|
+
const promptPromise = Object.assign(promise
|
|
92
|
+
.finally(() => {
|
|
93
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
94
|
+
screen.done({ clearContent: Boolean(context.clearPromptOnDone) });
|
|
95
|
+
output.end();
|
|
96
|
+
})
|
|
97
|
+
.then(() => promise), { cancel: () => rejectPrompt(new CancelPromptError()) });
|
|
98
|
+
if (signal) {
|
|
99
|
+
const abort = () => rejectPrompt(new AbortPromptError({ cause: signal.reason }));
|
|
100
|
+
if (signal.aborted) {
|
|
101
|
+
abort();
|
|
102
|
+
return promptPromise;
|
|
103
|
+
}
|
|
104
|
+
cleanups.add(listenTo(signal, 'abort', abort));
|
|
105
|
+
}
|
|
106
|
+
cleanups.add(onSignalExit((code, signal) => {
|
|
107
|
+
rejectPrompt(new ExitPromptError(`User force closed the prompt with ${code} ${signal}`));
|
|
108
|
+
}));
|
|
109
|
+
// SIGINT must be explicitly handled by the prompt so the ExitPromptError can be handled.
|
|
110
|
+
// Otherwise, the prompt will stop and in some scenarios never resolve.
|
|
111
|
+
// Ref issue #1741
|
|
112
|
+
cleanups.add(listenTo(rl, 'SIGINT', () => rejectPrompt(new ExitPromptError(`User force closed the prompt with SIGINT`))));
|
|
113
|
+
// Fallback for readline closing without the prompt settling (e.g. stdin
|
|
114
|
+
// EOF), so active effect timeouts still get cleared. Every settlement
|
|
115
|
+
// path already clears effects through settlePrompt; this event triggers
|
|
116
|
+
// immediately when the user presses ctrl+c, while signal-exit only
|
|
117
|
+
// triggers after the process is done (after timeouts finish triggering).
|
|
118
|
+
cleanups.add(listenTo(rl, 'close', clearEffects));
|
|
119
|
+
const startCycle = () => {
|
|
120
|
+
// Re-renders only happen when the state change; but the readline cursor could
|
|
121
|
+
// change position and that also requires a re-render (and a manual one because
|
|
122
|
+
// we mute the streams). We set the listener after the initial workLoop to avoid
|
|
123
|
+
// a double render if render triggered by a state change sets the cursor to the
|
|
124
|
+
// right position.
|
|
125
|
+
cleanups.add(listenTo(rl.input, 'keypress', () => screen.checkCursorPos()));
|
|
126
|
+
let pendingDone = null;
|
|
127
|
+
cycle(() => {
|
|
128
|
+
let effectsSettled = false;
|
|
129
|
+
try {
|
|
130
|
+
const nextView = view(config, (value) => {
|
|
131
|
+
if (effectsSettled) {
|
|
132
|
+
// After the cycle completes (async validation path), the "done"
|
|
133
|
+
// render already flushed via setStatus → handleChange, so resolve
|
|
134
|
+
// immediately.
|
|
135
|
+
resolvePrompt(value);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
pendingDone = { value };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// Typescript won't allow this, but not all users rely on typescript.
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
143
|
+
if (nextView === undefined) {
|
|
144
|
+
let callerFilename = callSites[1]?.getFileName();
|
|
145
|
+
if (callerFilename && !callerFilename.startsWith('file://')) {
|
|
146
|
+
callerFilename = path.resolve(callerFilename);
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Prompt functions must return a string.\n at ${callerFilename}`);
|
|
149
|
+
}
|
|
150
|
+
const [content, bottomContent] = typeof nextView === 'string' ? [nextView] : nextView;
|
|
151
|
+
screen.render(content, bottomContent);
|
|
152
|
+
effectScheduler.run();
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
rejectPrompt(error);
|
|
156
|
+
}
|
|
157
|
+
effectsSettled = true;
|
|
158
|
+
if (pendingDone !== null) {
|
|
159
|
+
const { value } = pendingDone;
|
|
160
|
+
pendingDone = null;
|
|
161
|
+
resolvePrompt(value);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
// Proper Readable streams (like process.stdin) may have OS-level buffered
|
|
166
|
+
// data that arrives in the poll phase when readline resumes the stream.
|
|
167
|
+
// Deferring the first render by one setImmediate tick (check phase, after
|
|
168
|
+
// poll) lets that stale data flow through readline harmlessly—no keypress
|
|
169
|
+
// handlers are registered yet and the output is muted, so the stale
|
|
170
|
+
// keystrokes are silently discarded.
|
|
171
|
+
// Old-style streams (like MuteStream) have no such buffering, so the
|
|
172
|
+
// render cycle starts immediately.
|
|
173
|
+
//
|
|
174
|
+
// @see https://github.com/SBoudrias/Inquirer.js/issues/1303
|
|
175
|
+
if ('readableFlowing' in input) {
|
|
176
|
+
nativeSetImmediate(startCycle);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
startCycle();
|
|
180
|
+
}
|
|
181
|
+
return promptPromise;
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
return prompt;
|
|
185
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class AbortPromptError extends Error {
|
|
2
|
+
name: string;
|
|
3
|
+
message: string;
|
|
4
|
+
constructor(options?: {
|
|
5
|
+
cause?: unknown;
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export declare class CancelPromptError extends Error {
|
|
9
|
+
name: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class ExitPromptError extends Error {
|
|
13
|
+
name: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class HookError extends Error {
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class ValidationError extends Error {
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class AbortPromptError extends Error {
|
|
2
|
+
name = 'AbortPromptError';
|
|
3
|
+
message = 'Prompt was aborted';
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super();
|
|
6
|
+
this.cause = options?.cause;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class CancelPromptError extends Error {
|
|
10
|
+
name = 'CancelPromptError';
|
|
11
|
+
message = 'Prompt was canceled';
|
|
12
|
+
}
|
|
13
|
+
export class ExitPromptError extends Error {
|
|
14
|
+
name = 'ExitPromptError';
|
|
15
|
+
}
|
|
16
|
+
export class HookError extends Error {
|
|
17
|
+
name = 'HookError';
|
|
18
|
+
}
|
|
19
|
+
export class ValidationError extends Error {
|
|
20
|
+
name = 'ValidationError';
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { InquirerReadline } from '@inquirer/type';
|
|
2
|
+
export declare function withHooks<T>(rl: InquirerReadline, cb: (cycle: (render: () => void) => void) => T): T;
|
|
3
|
+
export declare function readline(): InquirerReadline;
|
|
4
|
+
export declare function withUpdates<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R;
|
|
5
|
+
type Pointer<Value> = {
|
|
6
|
+
get(): Value;
|
|
7
|
+
set(value: Value): void;
|
|
8
|
+
initialized: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function withPointer<Value, ReturnValue>(cb: (pointer: Pointer<Value>) => ReturnValue): ReturnValue;
|
|
11
|
+
export declare function handleChange(): void;
|
|
12
|
+
export declare const effectScheduler: {
|
|
13
|
+
queue(cb: (readline: InquirerReadline) => void | (() => void)): void;
|
|
14
|
+
run(): void;
|
|
15
|
+
clearAll(): void;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* eslint @typescript-eslint/no-explicit-any: ["off"] */
|
|
2
|
+
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';
|
|
3
|
+
import { HookError, ValidationError } from "./errors.js";
|
|
4
|
+
const hookStorage = new AsyncLocalStorage();
|
|
5
|
+
function createStore(rl) {
|
|
6
|
+
const store = {
|
|
7
|
+
rl,
|
|
8
|
+
hooks: [],
|
|
9
|
+
hooksCleanup: [],
|
|
10
|
+
hooksEffect: [],
|
|
11
|
+
index: 0,
|
|
12
|
+
handleChange() { },
|
|
13
|
+
};
|
|
14
|
+
return store;
|
|
15
|
+
}
|
|
16
|
+
// Run callback in with the hook engine setup.
|
|
17
|
+
export function withHooks(rl, cb) {
|
|
18
|
+
const store = createStore(rl);
|
|
19
|
+
function cycle(render) {
|
|
20
|
+
store.handleChange = () => {
|
|
21
|
+
store.index = 0;
|
|
22
|
+
render();
|
|
23
|
+
};
|
|
24
|
+
store.handleChange();
|
|
25
|
+
}
|
|
26
|
+
return hookStorage.run(store, () => cb(cycle));
|
|
27
|
+
}
|
|
28
|
+
// Safe getStore utility that'll return the store or throw if undefined.
|
|
29
|
+
function getStore() {
|
|
30
|
+
const store = hookStorage.getStore();
|
|
31
|
+
if (!store) {
|
|
32
|
+
throw new HookError('[Inquirer] Hook functions can only be called from within a prompt');
|
|
33
|
+
}
|
|
34
|
+
return store;
|
|
35
|
+
}
|
|
36
|
+
export function readline() {
|
|
37
|
+
return getStore().rl;
|
|
38
|
+
}
|
|
39
|
+
// Merge state updates happening within the callback function to avoid multiple renders.
|
|
40
|
+
export function withUpdates(fn) {
|
|
41
|
+
const wrapped = (...args) => {
|
|
42
|
+
const store = getStore();
|
|
43
|
+
let shouldUpdate = false;
|
|
44
|
+
const oldHandleChange = store.handleChange;
|
|
45
|
+
store.handleChange = () => {
|
|
46
|
+
shouldUpdate = true;
|
|
47
|
+
};
|
|
48
|
+
const returnValue = fn(...args);
|
|
49
|
+
if (shouldUpdate) {
|
|
50
|
+
oldHandleChange();
|
|
51
|
+
}
|
|
52
|
+
store.handleChange = oldHandleChange;
|
|
53
|
+
return returnValue;
|
|
54
|
+
};
|
|
55
|
+
return AsyncResource.bind(wrapped);
|
|
56
|
+
}
|
|
57
|
+
export function withPointer(cb) {
|
|
58
|
+
const store = getStore();
|
|
59
|
+
const { index } = store;
|
|
60
|
+
const pointer = {
|
|
61
|
+
get() {
|
|
62
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
63
|
+
return store.hooks[index];
|
|
64
|
+
},
|
|
65
|
+
set(value) {
|
|
66
|
+
store.hooks[index] = value;
|
|
67
|
+
},
|
|
68
|
+
initialized: index in store.hooks,
|
|
69
|
+
};
|
|
70
|
+
const returnValue = cb(pointer);
|
|
71
|
+
store.index++;
|
|
72
|
+
return returnValue;
|
|
73
|
+
}
|
|
74
|
+
export function handleChange() {
|
|
75
|
+
getStore().handleChange();
|
|
76
|
+
}
|
|
77
|
+
export const effectScheduler = {
|
|
78
|
+
queue(cb) {
|
|
79
|
+
const store = getStore();
|
|
80
|
+
const { index } = store;
|
|
81
|
+
store.hooksEffect.push(() => {
|
|
82
|
+
store.hooksCleanup[index]?.();
|
|
83
|
+
const cleanFn = cb(readline());
|
|
84
|
+
if (cleanFn != null && typeof cleanFn !== 'function') {
|
|
85
|
+
throw new ValidationError('useEffect return value must be a cleanup function or nothing.');
|
|
86
|
+
}
|
|
87
|
+
store.hooksCleanup[index] = cleanFn;
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
run() {
|
|
91
|
+
const store = getStore();
|
|
92
|
+
withUpdates(() => {
|
|
93
|
+
store.hooksEffect.forEach((effect) => {
|
|
94
|
+
effect();
|
|
95
|
+
});
|
|
96
|
+
// Warning: Clean the hooks before exiting the `withUpdates` block.
|
|
97
|
+
// Failure to do so means an updates would hit the same effects again.
|
|
98
|
+
store.hooksEffect.length = 0;
|
|
99
|
+
})();
|
|
100
|
+
},
|
|
101
|
+
clearAll() {
|
|
102
|
+
const store = getStore();
|
|
103
|
+
store.hooksCleanup.forEach((cleanFn) => {
|
|
104
|
+
cleanFn?.();
|
|
105
|
+
});
|
|
106
|
+
store.hooksEffect.length = 0;
|
|
107
|
+
store.hooksCleanup.length = 0;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type KeypressEvent = {
|
|
2
|
+
name: string;
|
|
3
|
+
ctrl: boolean;
|
|
4
|
+
shift: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type Keybinding = 'emacs' | 'vim';
|
|
7
|
+
export declare function getDefaultKeybindings(): ReadonlyArray<Keybinding>;
|
|
8
|
+
export declare const isUpKey: (key: KeypressEvent, keybindings?: ReadonlyArray<Keybinding>) => boolean;
|
|
9
|
+
export declare const isDownKey: (key: KeypressEvent, keybindings?: ReadonlyArray<Keybinding>) => boolean;
|
|
10
|
+
export declare const isSpaceKey: (key: KeypressEvent) => boolean;
|
|
11
|
+
export declare const isBackspaceKey: (key: KeypressEvent) => boolean;
|
|
12
|
+
export declare const isTabKey: (key: KeypressEvent) => boolean;
|
|
13
|
+
export declare const isNumberKey: (key: KeypressEvent) => boolean;
|
|
14
|
+
export declare const isEnterKey: (key: KeypressEvent) => boolean;
|
|
15
|
+
export declare const isShiftKey: (key: KeypressEvent) => boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const keybindings = ['emacs', 'vim'];
|
|
2
|
+
const keybindingLookup = new Set(keybindings);
|
|
3
|
+
function isKeybinding(value) {
|
|
4
|
+
return keybindingLookup.has(value);
|
|
5
|
+
}
|
|
6
|
+
export function getDefaultKeybindings() {
|
|
7
|
+
const env = process.env['INQUIRER_KEYBINDINGS'];
|
|
8
|
+
if (!env)
|
|
9
|
+
return [];
|
|
10
|
+
return Array.from(new Set(env
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.split(/[\s,]+/)
|
|
13
|
+
.filter(isKeybinding)));
|
|
14
|
+
}
|
|
15
|
+
export const isUpKey = (key, keybindings = []) =>
|
|
16
|
+
// The up key
|
|
17
|
+
key.name === 'up' ||
|
|
18
|
+
// Vim keybinding: hjkl keys map to left/down/up/right
|
|
19
|
+
(keybindings.includes('vim') && key.name === 'k') ||
|
|
20
|
+
// Emacs keybinding: Ctrl+P means "previous" in Emacs navigation conventions
|
|
21
|
+
(keybindings.includes('emacs') && key.ctrl && key.name === 'p');
|
|
22
|
+
export const isDownKey = (key, keybindings = []) =>
|
|
23
|
+
// The down key
|
|
24
|
+
key.name === 'down' ||
|
|
25
|
+
// Vim keybinding: hjkl keys map to left/down/up/right
|
|
26
|
+
(keybindings.includes('vim') && key.name === 'j') ||
|
|
27
|
+
// Emacs keybinding: Ctrl+N means "next" in Emacs navigation conventions
|
|
28
|
+
(keybindings.includes('emacs') && key.ctrl && key.name === 'n');
|
|
29
|
+
export const isSpaceKey = (key) => key.name === 'space';
|
|
30
|
+
export const isBackspaceKey = (key) => key.name === 'backspace';
|
|
31
|
+
export const isTabKey = (key) => key.name === 'tab';
|
|
32
|
+
export const isNumberKey = (key) => '1234567890'.includes(key.name);
|
|
33
|
+
export const isEnterKey = (key) => key.name === 'enter' || key.name === 'return';
|
|
34
|
+
export const isShiftKey = (key) => key.shift;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getDefaultTheme } from "./theme.js";
|
|
2
|
+
function isPlainObject(value) {
|
|
3
|
+
if (typeof value !== 'object' || value === null)
|
|
4
|
+
return false;
|
|
5
|
+
let proto = value;
|
|
6
|
+
while (Object.getPrototypeOf(proto) !== null) {
|
|
7
|
+
proto = Object.getPrototypeOf(proto);
|
|
8
|
+
}
|
|
9
|
+
return Object.getPrototypeOf(value) === proto;
|
|
10
|
+
}
|
|
11
|
+
function deepMerge(...objects) {
|
|
12
|
+
const output = {};
|
|
13
|
+
for (const obj of objects) {
|
|
14
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
15
|
+
const prevValue = output[key];
|
|
16
|
+
output[key] =
|
|
17
|
+
isPlainObject(prevValue) && isPlainObject(value)
|
|
18
|
+
? deepMerge(prevValue, value)
|
|
19
|
+
: value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
23
|
+
return output;
|
|
24
|
+
}
|
|
25
|
+
export function makeTheme(...themes) {
|
|
26
|
+
/* oxlint-disable typescript/no-unsafe-type-assertion */
|
|
27
|
+
const themesToMerge = [
|
|
28
|
+
getDefaultTheme(),
|
|
29
|
+
...themes.filter((theme) => theme != null),
|
|
30
|
+
];
|
|
31
|
+
return deepMerge(...themesToMerge);
|
|
32
|
+
/* oxlint-enable typescript/no-unsafe-type-assertion */
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function usePagination<T>({ items, active, renderItem, pageSize, loop, }: {
|
|
2
|
+
items: ReadonlyArray<T>;
|
|
3
|
+
/** The index of the active item. */
|
|
4
|
+
active: number;
|
|
5
|
+
/** Renders an item as part of a page. */
|
|
6
|
+
renderItem: (layout: {
|
|
7
|
+
item: T;
|
|
8
|
+
index: number;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
}) => string;
|
|
11
|
+
/** The size of the page. */
|
|
12
|
+
pageSize: number;
|
|
13
|
+
/** Allows creating an infinitely looping list. `true` if unspecified. */
|
|
14
|
+
loop?: boolean;
|
|
15
|
+
}): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useRef } from "../use-ref.js";
|
|
2
|
+
import { readlineWidth, breakLines } from "../utils.js";
|
|
3
|
+
function usePointerPosition({ active, renderedItems, pageSize, loop, }) {
|
|
4
|
+
const state = useRef({
|
|
5
|
+
lastPointer: active,
|
|
6
|
+
lastActive: undefined,
|
|
7
|
+
});
|
|
8
|
+
const { lastPointer, lastActive } = state.current;
|
|
9
|
+
const middle = Math.floor(pageSize / 2);
|
|
10
|
+
const renderedLength = renderedItems.reduce((acc, item) => acc + item.length, 0);
|
|
11
|
+
const defaultPointerPosition = renderedItems
|
|
12
|
+
.slice(0, active)
|
|
13
|
+
.reduce((acc, item) => acc + item.length, 0);
|
|
14
|
+
let pointer = defaultPointerPosition;
|
|
15
|
+
if (renderedLength > pageSize) {
|
|
16
|
+
if (loop) {
|
|
17
|
+
/**
|
|
18
|
+
* Creates the next position for the pointer considering an infinitely
|
|
19
|
+
* looping list of items to be rendered on the page.
|
|
20
|
+
*
|
|
21
|
+
* The goal is to progressively move the cursor to the middle position as the user move down, and then keep
|
|
22
|
+
* the cursor there. When the user move up, maintain the cursor position.
|
|
23
|
+
*/
|
|
24
|
+
// By default, keep the cursor position as-is.
|
|
25
|
+
pointer = lastPointer;
|
|
26
|
+
if (
|
|
27
|
+
// First render, skip this logic.
|
|
28
|
+
lastActive != null &&
|
|
29
|
+
// Only move the pointer down when the user moves down.
|
|
30
|
+
lastActive < active &&
|
|
31
|
+
// Check user didn't move up across page boundary.
|
|
32
|
+
active - lastActive < pageSize) {
|
|
33
|
+
pointer = Math.min(
|
|
34
|
+
// Furthest allowed position for the pointer is the middle of the list
|
|
35
|
+
middle, Math.abs(active - lastActive) === 1
|
|
36
|
+
? Math.min(
|
|
37
|
+
// Move the pointer at most the height of the last active item.
|
|
38
|
+
lastPointer + (renderedItems[lastActive]?.length ?? 0),
|
|
39
|
+
// If the user moved by one item, move the pointer to the natural position of the active item as
|
|
40
|
+
// long as it doesn't move the cursor up.
|
|
41
|
+
Math.max(defaultPointerPosition, lastPointer))
|
|
42
|
+
: // Otherwise, move the pointer down by the difference between the active and last active item.
|
|
43
|
+
lastPointer + active - lastActive);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
/**
|
|
48
|
+
* Creates the next position for the pointer considering a finite list of
|
|
49
|
+
* items to be rendered on a page.
|
|
50
|
+
*
|
|
51
|
+
* The goal is to keep the pointer in the middle of the page whenever possible, until
|
|
52
|
+
* we reach the bounds of the list (top or bottom). In which case, the cursor moves progressively
|
|
53
|
+
* to the bottom or top of the list.
|
|
54
|
+
*/
|
|
55
|
+
const spaceUnderActive = renderedItems
|
|
56
|
+
.slice(active)
|
|
57
|
+
.reduce((acc, item) => acc + item.length, 0);
|
|
58
|
+
pointer =
|
|
59
|
+
spaceUnderActive < pageSize - middle
|
|
60
|
+
? // If the active item is near the end of the list, progressively move the cursor towards the end.
|
|
61
|
+
pageSize - spaceUnderActive
|
|
62
|
+
: // Otherwise, progressively move the pointer to the middle of the list.
|
|
63
|
+
Math.min(defaultPointerPosition, middle);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Save state for the next render
|
|
67
|
+
state.current.lastPointer = pointer;
|
|
68
|
+
state.current.lastActive = active;
|
|
69
|
+
return pointer;
|
|
70
|
+
}
|
|
71
|
+
export function usePagination({ items, active, renderItem, pageSize, loop = true, }) {
|
|
72
|
+
const width = readlineWidth();
|
|
73
|
+
const bound = (num) => ((num % items.length) + items.length) % items.length;
|
|
74
|
+
const renderedItems = items.map((item, index) => {
|
|
75
|
+
if (item == null)
|
|
76
|
+
return [];
|
|
77
|
+
return breakLines(renderItem({ item, index, isActive: index === active }), width).split('\n');
|
|
78
|
+
});
|
|
79
|
+
const renderedLength = renderedItems.reduce((acc, item) => acc + item.length, 0);
|
|
80
|
+
const renderItemAtIndex = (index) => renderedItems[index] ?? [];
|
|
81
|
+
const pointer = usePointerPosition({ active, renderedItems, pageSize, loop });
|
|
82
|
+
// Render the active item to decide the position.
|
|
83
|
+
// If the active item fits under the pointer, we render it there.
|
|
84
|
+
// Otherwise, we need to render it to fit at the bottom of the page; moving the pointer up.
|
|
85
|
+
const activeItem = renderItemAtIndex(active).slice(0, pageSize);
|
|
86
|
+
const activeItemPosition = pointer + activeItem.length <= pageSize ? pointer : pageSize - activeItem.length;
|
|
87
|
+
// Create an array of lines for the page, and add the lines of the active item into the page
|
|
88
|
+
const pageBuffer = Array.from({ length: pageSize });
|
|
89
|
+
pageBuffer.splice(activeItemPosition, activeItem.length, ...activeItem);
|
|
90
|
+
// Store to prevent rendering the same item twice
|
|
91
|
+
const itemVisited = new Set([active]);
|
|
92
|
+
// Fill the page under the active item
|
|
93
|
+
let bufferPointer = activeItemPosition + activeItem.length;
|
|
94
|
+
let itemPointer = bound(active + 1);
|
|
95
|
+
while (bufferPointer < pageSize &&
|
|
96
|
+
!itemVisited.has(itemPointer) &&
|
|
97
|
+
(loop && renderedLength > pageSize ? itemPointer !== active : itemPointer > active)) {
|
|
98
|
+
const lines = renderItemAtIndex(itemPointer);
|
|
99
|
+
const linesToAdd = lines.slice(0, pageSize - bufferPointer);
|
|
100
|
+
pageBuffer.splice(bufferPointer, linesToAdd.length, ...linesToAdd);
|
|
101
|
+
// Move pointers for next iteration
|
|
102
|
+
itemVisited.add(itemPointer);
|
|
103
|
+
bufferPointer += linesToAdd.length;
|
|
104
|
+
itemPointer = bound(itemPointer + 1);
|
|
105
|
+
}
|
|
106
|
+
// Fill the page over the active item
|
|
107
|
+
bufferPointer = activeItemPosition - 1;
|
|
108
|
+
itemPointer = bound(active - 1);
|
|
109
|
+
while (bufferPointer >= 0 &&
|
|
110
|
+
!itemVisited.has(itemPointer) &&
|
|
111
|
+
(loop && renderedLength > pageSize ? itemPointer !== active : itemPointer < active)) {
|
|
112
|
+
const lines = renderItemAtIndex(itemPointer);
|
|
113
|
+
const linesToAdd = lines.slice(Math.max(0, lines.length - bufferPointer - 1));
|
|
114
|
+
pageBuffer.splice(bufferPointer - linesToAdd.length + 1, linesToAdd.length, ...linesToAdd);
|
|
115
|
+
// Move pointers for next iteration
|
|
116
|
+
itemVisited.add(itemPointer);
|
|
117
|
+
bufferPointer -= linesToAdd.length;
|
|
118
|
+
itemPointer = bound(itemPointer - 1);
|
|
119
|
+
}
|
|
120
|
+
return pageBuffer.filter((line) => typeof line === 'string').join('\n');
|
|
121
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InquirerReadline } from '@inquirer/type';
|
|
2
|
+
export default class ScreenManager {
|
|
3
|
+
private height;
|
|
4
|
+
private extraLinesUnderPrompt;
|
|
5
|
+
private cursorPos;
|
|
6
|
+
private readonly rl;
|
|
7
|
+
constructor(rl: InquirerReadline);
|
|
8
|
+
write(content: string): void;
|
|
9
|
+
render(content: string, bottomContent?: string): void;
|
|
10
|
+
checkCursorPos(): void;
|
|
11
|
+
done({ clearContent }: {
|
|
12
|
+
clearContent: boolean;
|
|
13
|
+
}): void;
|
|
14
|
+
}
|