@agentuity/cli 2.0.12 → 2.0.14
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/agent-detection.d.ts.map +1 -1
- package/dist/agent-detection.js +1 -0
- package/dist/agent-detection.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +15 -8
- package/dist/cli.js.map +1 -1
- package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/create.js +46 -4
- package/dist/cmd/cloud/sandbox/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.js +4 -3
- package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
- package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/run.js +9 -5
- package/dist/cmd/cloud/sandbox/run.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
- package/dist/cmd/coder/start.d.ts.map +1 -1
- package/dist/cmd/coder/start.js +1 -0
- package/dist/cmd/coder/start.js.map +1 -1
- package/dist/cmd/coder/workspace/common.d.ts +29 -0
- package/dist/cmd/coder/workspace/common.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/common.js +83 -0
- package/dist/cmd/coder/workspace/common.js.map +1 -0
- package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/create.js +34 -37
- package/dist/cmd/coder/workspace/create.js.map +1 -1
- package/dist/cmd/coder/workspace/get.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/get.js +2 -5
- package/dist/cmd/coder/workspace/get.js.map +1 -1
- package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/index.js +10 -0
- package/dist/cmd/coder/workspace/index.js.map +1 -1
- package/dist/cmd/coder/workspace/list.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/list.js +4 -0
- package/dist/cmd/coder/workspace/list.js.map +1 -1
- package/dist/cmd/coder/workspace/refresh.d.ts +2 -0
- package/dist/cmd/coder/workspace/refresh.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/refresh.js +59 -0
- package/dist/cmd/coder/workspace/refresh.js.map +1 -0
- package/dist/cmd/coder/workspace/update.d.ts +2 -0
- package/dist/cmd/coder/workspace/update.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/update.js +131 -0
- package/dist/cmd/coder/workspace/update.js.map +1 -0
- package/dist/cmd/coder/workspace/validate-dependencies.d.ts +2 -0
- package/dist/cmd/coder/workspace/validate-dependencies.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/validate-dependencies.js +70 -0
- package/dist/cmd/coder/workspace/validate-dependencies.js.map +1 -0
- package/dist/cmd/project/random-name.d.ts +17 -0
- package/dist/cmd/project/random-name.d.ts.map +1 -0
- package/dist/cmd/project/random-name.js +144 -0
- package/dist/cmd/project/random-name.js.map +1 -0
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +181 -153
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/composite-logger.d.ts.map +1 -1
- package/dist/composite-logger.js +19 -0
- package/dist/composite-logger.js.map +1 -1
- package/dist/config.d.ts +18 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -16
- package/dist/config.js.map +1 -1
- package/dist/tui/prompt.d.ts +29 -0
- package/dist/tui/prompt.d.ts.map +1 -1
- package/dist/tui/prompt.js +180 -8
- package/dist/tui/prompt.js.map +1 -1
- package/package.json +7 -7
- package/src/agent-detection.ts +1 -0
- package/src/cli.ts +30 -8
- package/src/cmd/cloud/sandbox/create.ts +57 -4
- package/src/cmd/cloud/sandbox/exec.ts +4 -3
- package/src/cmd/cloud/sandbox/run.ts +9 -5
- package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
- package/src/cmd/coder/start.ts +1 -0
- package/src/cmd/coder/workspace/common.ts +103 -0
- package/src/cmd/coder/workspace/create.ts +39 -43
- package/src/cmd/coder/workspace/get.ts +2 -5
- package/src/cmd/coder/workspace/index.ts +10 -0
- package/src/cmd/coder/workspace/list.ts +4 -0
- package/src/cmd/coder/workspace/refresh.ts +63 -0
- package/src/cmd/coder/workspace/update.ts +154 -0
- package/src/cmd/coder/workspace/validate-dependencies.ts +75 -0
- package/src/cmd/project/random-name.ts +152 -0
- package/src/cmd/project/template-flow.ts +199 -161
- package/src/composite-logger.ts +20 -0
- package/src/config.ts +69 -19
- package/src/tui/prompt.ts +214 -8
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { chmod, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { basename, extname, join, normalize, resolve } from 'node:path';
|
|
4
|
+
import { basename, dirname, extname, join, normalize, resolve } from 'node:path';
|
|
5
5
|
import { type Logger, StructuredError } from '@agentuity/core';
|
|
6
6
|
import {
|
|
7
7
|
type BuildMetadata,
|
|
@@ -15,6 +15,7 @@ import { z } from 'zod';
|
|
|
15
15
|
import { clearProfileCache } from './cache';
|
|
16
16
|
import { getCatalystUrl } from './catalyst';
|
|
17
17
|
import { readEnvFile, writeEnvFile } from './env-util';
|
|
18
|
+
import { ErrorCode } from './errors';
|
|
18
19
|
import {
|
|
19
20
|
deleteAuthFromKeychain,
|
|
20
21
|
getAuthFromKeychain,
|
|
@@ -151,10 +152,12 @@ function expandTilde(path: string): string {
|
|
|
151
152
|
let cachedConfig: Config | null | undefined;
|
|
152
153
|
// Track the resolved config path so saveConfig writes back to the same file
|
|
153
154
|
let cachedConfigPath: string | undefined;
|
|
155
|
+
const loadedProjectConfigPaths = new Map<string, string>();
|
|
154
156
|
|
|
155
157
|
export function resetConfigCache(): void {
|
|
156
158
|
cachedConfig = undefined;
|
|
157
159
|
cachedConfigPath = undefined;
|
|
160
|
+
loadedProjectConfigPaths.clear();
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
export async function loadConfig(
|
|
@@ -600,31 +603,76 @@ export function generateYAMLTemplate(name: string): string {
|
|
|
600
603
|
return lines.join('\n');
|
|
601
604
|
}
|
|
602
605
|
|
|
603
|
-
export const ProjectConfigNotFoundException = StructuredError('ProjectConfigNotFoundException')
|
|
606
|
+
export const ProjectConfigNotFoundException = StructuredError('ProjectConfigNotFoundException')<{
|
|
607
|
+
code?: ErrorCode;
|
|
608
|
+
configPath?: string;
|
|
609
|
+
explicit?: boolean;
|
|
610
|
+
}>();
|
|
604
611
|
|
|
605
612
|
type ProjectConfig = z.infer<typeof ProjectSchema>;
|
|
606
613
|
|
|
607
|
-
export
|
|
608
|
-
|
|
614
|
+
export type ResolvedProjectConfigPaths = {
|
|
615
|
+
projectDir: string;
|
|
616
|
+
configPath: string;
|
|
617
|
+
explicitConfigFile: boolean;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export async function resolveProjectConfigPaths(
|
|
621
|
+
path: string,
|
|
609
622
|
config?: Config | null
|
|
610
|
-
): Promise<
|
|
611
|
-
|
|
623
|
+
): Promise<ResolvedProjectConfigPaths> {
|
|
624
|
+
const resolvedPath = resolve(expandTilde(path));
|
|
625
|
+
const pathStats = await stat(resolvedPath).catch(() => null);
|
|
626
|
+
const isExplicitJsonFile =
|
|
627
|
+
(pathStats?.isFile() || pathStats === null) && extname(resolvedPath) === '.json';
|
|
628
|
+
|
|
629
|
+
if (isExplicitJsonFile) {
|
|
630
|
+
return {
|
|
631
|
+
projectDir: dirname(resolvedPath),
|
|
632
|
+
configPath: resolvedPath,
|
|
633
|
+
explicitConfigFile: true,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const projectDir = resolvedPath;
|
|
638
|
+
let configPath = join(projectDir, 'agentuity.json');
|
|
612
639
|
|
|
613
|
-
// Check for profile-specific override if config is provided
|
|
614
640
|
if (config?.name) {
|
|
615
|
-
const profileConfigPath = join(
|
|
641
|
+
const profileConfigPath = join(projectDir, `agentuity.${config.name}.json`);
|
|
616
642
|
if (await Bun.file(profileConfigPath).exists()) {
|
|
617
643
|
configPath = profileConfigPath;
|
|
618
644
|
}
|
|
619
645
|
}
|
|
620
646
|
|
|
647
|
+
return {
|
|
648
|
+
projectDir,
|
|
649
|
+
configPath,
|
|
650
|
+
explicitConfigFile: false,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function loadProjectConfig(
|
|
655
|
+
dir: string,
|
|
656
|
+
config?: Config | null
|
|
657
|
+
): Promise<ProjectConfig> {
|
|
658
|
+
const { projectDir, configPath, explicitConfigFile } = await resolveProjectConfigPaths(
|
|
659
|
+
dir,
|
|
660
|
+
config
|
|
661
|
+
);
|
|
662
|
+
|
|
621
663
|
const file = Bun.file(configPath);
|
|
622
664
|
if (!(await file.exists())) {
|
|
623
665
|
// TODO: check to see if a valid project that was created unauthenticated
|
|
624
666
|
// and then if so:
|
|
625
667
|
// 1. if authentication, offer to import the project
|
|
626
668
|
// 2. tell them that they need to login to use the command and import the project
|
|
627
|
-
throw new ProjectConfigNotFoundException({
|
|
669
|
+
throw new ProjectConfigNotFoundException({
|
|
670
|
+
message: explicitConfigFile
|
|
671
|
+
? `Project config not found at ${configPath}`
|
|
672
|
+
: 'project config not found',
|
|
673
|
+
configPath,
|
|
674
|
+
explicit: explicitConfigFile,
|
|
675
|
+
});
|
|
628
676
|
}
|
|
629
677
|
const text = await file.text();
|
|
630
678
|
const parsedConfig = parseJSONC(text);
|
|
@@ -637,6 +685,7 @@ export async function loadProjectConfig(
|
|
|
637
685
|
}
|
|
638
686
|
process.exit(1);
|
|
639
687
|
}
|
|
688
|
+
loadedProjectConfigPaths.set(projectDir, configPath);
|
|
640
689
|
return result.data;
|
|
641
690
|
}
|
|
642
691
|
|
|
@@ -717,18 +766,19 @@ export async function updateProjectConfig(
|
|
|
717
766
|
updates: Partial<z.infer<typeof ProjectSchema>>,
|
|
718
767
|
config?: Config | null
|
|
719
768
|
): Promise<void> {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (await Bun.file(profileConfigPath).exists()) {
|
|
725
|
-
configPath = profileConfigPath;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
769
|
+
const resolved = await resolveProjectConfigPaths(dir, config);
|
|
770
|
+
const configPath = resolved.explicitConfigFile
|
|
771
|
+
? resolved.configPath
|
|
772
|
+
: (loadedProjectConfigPaths.get(resolved.projectDir) ?? resolved.configPath);
|
|
728
773
|
|
|
729
774
|
const file = Bun.file(configPath);
|
|
730
775
|
if (!(await file.exists())) {
|
|
731
|
-
throw new
|
|
776
|
+
throw new ProjectConfigNotFoundException({
|
|
777
|
+
code: ErrorCode.PROJECT_NOT_FOUND,
|
|
778
|
+
message: `Project config not found at ${configPath}`,
|
|
779
|
+
configPath: resolved.configPath,
|
|
780
|
+
explicit: resolved.explicitConfigFile,
|
|
781
|
+
});
|
|
732
782
|
}
|
|
733
783
|
|
|
734
784
|
const text = await file.text();
|
package/src/tui/prompt.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface TextOptions {
|
|
|
14
14
|
message: string;
|
|
15
15
|
initial?: string;
|
|
16
16
|
hint?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Pre-generated suggestion shown dim in the hint line.
|
|
19
|
+
* Submitting empty input accepts the placeholder.
|
|
20
|
+
* Unlike `initial`, this is rendered visibly so the user knows what they'll get.
|
|
21
|
+
*/
|
|
22
|
+
placeholder?: string;
|
|
17
23
|
validate?: (value: string) => boolean | string | Promise<boolean | string>;
|
|
18
24
|
}
|
|
19
25
|
|
|
@@ -92,27 +98,39 @@ export class PromptFlow {
|
|
|
92
98
|
|
|
93
99
|
/**
|
|
94
100
|
* Text input prompt
|
|
101
|
+
*
|
|
102
|
+
* Two render paths:
|
|
103
|
+
* - With `placeholder`: custom raw-keypress renderer that paints the placeholder
|
|
104
|
+
* inline as dim ghost text at the cursor (autofill style). Vanishes on the first
|
|
105
|
+
* keystroke; Enter on an empty buffer accepts the placeholder.
|
|
106
|
+
* - Without `placeholder`: original readline-based renderer (untouched).
|
|
95
107
|
*/
|
|
96
108
|
async text(options: TextOptions): Promise<string> {
|
|
97
|
-
const { message, validate } = options;
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
const { message, validate, placeholder } = options;
|
|
110
|
+
// `placeholder` acts as a visible default: empty submit resolves to it.
|
|
111
|
+
// `initial` (legacy, invisible) still works but `placeholder` takes precedence.
|
|
112
|
+
const fallback = placeholder ?? options.initial ?? '';
|
|
113
|
+
const hasDefault = placeholder !== undefined || options.initial !== undefined;
|
|
100
114
|
|
|
101
115
|
if (!this.isInteractive()) {
|
|
102
116
|
if (hasDefault) {
|
|
103
|
-
const validationResult = validate ? await validate(
|
|
117
|
+
const validationResult = validate ? await validate(fallback) : true;
|
|
104
118
|
if (validationResult === true) {
|
|
105
|
-
return
|
|
119
|
+
return fallback;
|
|
106
120
|
}
|
|
107
121
|
// Validation failed - include the error message if it's a string
|
|
108
122
|
const errorDetail = typeof validationResult === 'string' ? `: ${validationResult}` : '';
|
|
109
123
|
throw this.nonInteractiveError(
|
|
110
|
-
`Cannot prompt for "${message}" in non-interactive mode. Validation failed for default value "${
|
|
124
|
+
`Cannot prompt for "${message}" in non-interactive mode. Validation failed for default value "${fallback}"${errorDetail}.`
|
|
111
125
|
);
|
|
112
126
|
}
|
|
113
127
|
throw this.nonInteractiveError(`Cannot prompt for "${message}" in non-interactive mode.`);
|
|
114
128
|
}
|
|
115
129
|
|
|
130
|
+
if (placeholder) {
|
|
131
|
+
return this.textWithGhost(options, placeholder);
|
|
132
|
+
}
|
|
133
|
+
|
|
116
134
|
return new Promise((resolve, reject) => {
|
|
117
135
|
const rl = readline.createInterface({
|
|
118
136
|
input: process.stdin,
|
|
@@ -139,8 +157,8 @@ export class PromptFlow {
|
|
|
139
157
|
|
|
140
158
|
rl.on('line', async (input) => {
|
|
141
159
|
const trimmed = input.trim();
|
|
142
|
-
// After a validation error, require explicit input - don't fall back to
|
|
143
|
-
const value = trimmed.length > 0 ? trimmed : hadValidationError ? '' :
|
|
160
|
+
// After a validation error, require explicit input - don't fall back to default.
|
|
161
|
+
const value = trimmed.length > 0 ? trimmed : hadValidationError ? '' : fallback;
|
|
144
162
|
|
|
145
163
|
// Validate
|
|
146
164
|
if (validate) {
|
|
@@ -229,6 +247,194 @@ export class PromptFlow {
|
|
|
229
247
|
});
|
|
230
248
|
}
|
|
231
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Text input with inline ghost-text placeholder (autofill style).
|
|
252
|
+
*
|
|
253
|
+
* Layout:
|
|
254
|
+
* ◆ <message>
|
|
255
|
+
* │ <hint> (optional)
|
|
256
|
+
* │ <typed>│<ghost> ghost is dim, vanishes on first keystroke
|
|
257
|
+
*
|
|
258
|
+
* Behavior:
|
|
259
|
+
* - Typing any char hides the ghost permanently for this prompt instance.
|
|
260
|
+
* - Backspacing back to empty does NOT bring the ghost back.
|
|
261
|
+
* - Enter on empty buffer → resolves to placeholder.
|
|
262
|
+
* - Enter with typed text → resolves to typed text.
|
|
263
|
+
* - Validation error: shows inline error, ghost stays gone, user must type.
|
|
264
|
+
* - Ctrl+C: cancels.
|
|
265
|
+
*/
|
|
266
|
+
private async textWithGhost(options: TextOptions, placeholder: string): Promise<string> {
|
|
267
|
+
const { message, validate, hint } = options;
|
|
268
|
+
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
let buffer = '';
|
|
271
|
+
// Tracks whether the ghost should still be visible. Once any printable key is
|
|
272
|
+
// pressed it goes false and stays false for the rest of this prompt.
|
|
273
|
+
let ghostVisible = true;
|
|
274
|
+
let hasError = false;
|
|
275
|
+
let errorMsg = '';
|
|
276
|
+
|
|
277
|
+
const inputPrefix = `${colors.secondary(symbols.bar)} `;
|
|
278
|
+
// Visible-character length of the input-line prefix ("│ " = 3 cells).
|
|
279
|
+
const PREFIX_VISIBLE = 3;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Repaint everything from the message line down. Cursor must be on the
|
|
283
|
+
* message line (column 0) when this is called for the first time, or we
|
|
284
|
+
* just moved up to it after a clear.
|
|
285
|
+
*/
|
|
286
|
+
const paint = () => {
|
|
287
|
+
const symbol = hasError ? colors.error(symbols.error) : colors.active(symbols.active);
|
|
288
|
+
process.stdout.write(`${symbol} ${message}\n`);
|
|
289
|
+
|
|
290
|
+
if (hasError) {
|
|
291
|
+
process.stdout.write(
|
|
292
|
+
`${colors.secondary(symbols.bar)} ${colors.error(errorMsg)}\n`
|
|
293
|
+
);
|
|
294
|
+
} else if (hint) {
|
|
295
|
+
process.stdout.write(`${colors.secondary(symbols.bar)} ${colors.muted(hint)}\n`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Input line.
|
|
299
|
+
process.stdout.write(inputPrefix);
|
|
300
|
+
process.stdout.write(buffer);
|
|
301
|
+
|
|
302
|
+
if (ghostVisible && buffer.length === 0) {
|
|
303
|
+
// Paint the ghost, then move the cursor back to the start of it so the
|
|
304
|
+
// caret sits where the user would start typing.
|
|
305
|
+
process.stdout.write(colors.muted(placeholder));
|
|
306
|
+
readline.moveCursor(process.stdout, -placeholder.length, 0);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Number of terminal lines currently occupied by our render, so we know
|
|
312
|
+
* how many to clear on the next repaint.
|
|
313
|
+
* Always: message (1) + hint-or-error (0/1) + input (1).
|
|
314
|
+
*/
|
|
315
|
+
const renderedLines = (): number => {
|
|
316
|
+
let n = 1; // message
|
|
317
|
+
if (hasError || hint) n += 1;
|
|
318
|
+
n += 1; // input
|
|
319
|
+
return n;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const repaint = () => {
|
|
323
|
+
// Move cursor to column 0, then up to the start of our render block, then clear.
|
|
324
|
+
readline.cursorTo(process.stdout, 0);
|
|
325
|
+
readline.moveCursor(process.stdout, 0, -(renderedLines() - 1));
|
|
326
|
+
readline.clearScreenDown(process.stdout);
|
|
327
|
+
paint();
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Resume stdin if it was paused by a prior prompt.
|
|
331
|
+
if (process.stdin.isTTY && process.stdin.isPaused()) {
|
|
332
|
+
process.stdin.resume();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
readline.emitKeypressEvents(process.stdin);
|
|
336
|
+
if (process.stdin.isTTY) {
|
|
337
|
+
process.stdin.setRawMode(true);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Initial paint.
|
|
341
|
+
paint();
|
|
342
|
+
|
|
343
|
+
const cleanup = () => {
|
|
344
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
345
|
+
if (process.stdin.isTTY) {
|
|
346
|
+
process.stdin.setRawMode(false);
|
|
347
|
+
process.stdin.pause();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const finalize = (value: string) => {
|
|
352
|
+
// Repaint as completed: replace whole render block with the completed lines.
|
|
353
|
+
readline.cursorTo(process.stdout, 0);
|
|
354
|
+
readline.moveCursor(process.stdout, 0, -(renderedLines() - 1));
|
|
355
|
+
readline.clearScreenDown(process.stdout);
|
|
356
|
+
|
|
357
|
+
if (value === '') {
|
|
358
|
+
process.stdout.write(
|
|
359
|
+
`${colors.completed(symbols.completed)} ${message}\n${colors.secondary(symbols.bar)}\n`
|
|
360
|
+
);
|
|
361
|
+
} else {
|
|
362
|
+
process.stdout.write(
|
|
363
|
+
`${colors.completed(symbols.completed)} ${message}\n${colors.secondary(symbols.bar)} ${colors.muted(value)}\n${colors.secondary(symbols.bar)}\n`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.states.push({ type: 'completed', message, value });
|
|
368
|
+
cleanup();
|
|
369
|
+
resolve(value);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const onKeypress = async (str: string, key: KeypressEvent) => {
|
|
373
|
+
if (key.ctrl && key.name === 'c') {
|
|
374
|
+
cleanup();
|
|
375
|
+
console.log('\n');
|
|
376
|
+
this.cancel('Operation cancelled');
|
|
377
|
+
reject(new Error('User cancelled'));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (key.name === 'return') {
|
|
382
|
+
const trimmed = buffer.trim();
|
|
383
|
+
// Empty submit accepts the placeholder, but only if no error has occurred
|
|
384
|
+
// (after an error the user must type explicitly — same rule the readline
|
|
385
|
+
// path uses).
|
|
386
|
+
const value = trimmed.length > 0 ? trimmed : hasError ? '' : placeholder;
|
|
387
|
+
|
|
388
|
+
if (validate) {
|
|
389
|
+
try {
|
|
390
|
+
const result = await validate(value);
|
|
391
|
+
if (result !== true) {
|
|
392
|
+
errorMsg = typeof result === 'string' ? result : 'Invalid input';
|
|
393
|
+
hasError = true;
|
|
394
|
+
ghostVisible = false;
|
|
395
|
+
repaint();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
} catch (err) {
|
|
399
|
+
errorMsg = err instanceof Error ? err.message : 'Validation failed';
|
|
400
|
+
hasError = true;
|
|
401
|
+
ghostVisible = false;
|
|
402
|
+
repaint();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
finalize(value);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (key.name === 'backspace') {
|
|
412
|
+
if (buffer.length > 0) {
|
|
413
|
+
buffer = buffer.slice(0, -1);
|
|
414
|
+
repaint();
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Printable single character (ignore arrow keys, function keys, etc.).
|
|
420
|
+
if (str && str.length === 1 && !key.ctrl && str >= ' ' && str !== '\x7f') {
|
|
421
|
+
buffer += str;
|
|
422
|
+
ghostVisible = false;
|
|
423
|
+
repaint();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Everything else (arrows, tab, etc.) is ignored.
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Mark the prefix length as used so the unused-var rule doesn't trip when we
|
|
431
|
+
// extend this in future. (Keeping it documented for cursor-math sanity.)
|
|
432
|
+
void PREFIX_VISIBLE;
|
|
433
|
+
|
|
434
|
+
process.stdin.on('keypress', onKeypress);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
232
438
|
/**
|
|
233
439
|
* Confirm (yes/no) prompt
|
|
234
440
|
*/
|