@super-protocol/sp-cli 0.0.7 → 0.0.9
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 +63 -44
- package/dist/commands/account/base.d.ts +1 -2
- package/dist/commands/account/base.js +8 -14
- package/dist/commands/account/forget.d.ts +1 -0
- package/dist/commands/account/forget.js +69 -3
- package/dist/commands/account/info.js +5 -5
- package/dist/commands/account/login.d.ts +6 -0
- package/dist/commands/account/login.js +122 -22
- package/dist/commands/account/switch.d.ts +3 -0
- package/dist/commands/account/switch.js +30 -10
- package/dist/commands/base.js +97 -12
- package/dist/commands/storage/base.js +11 -11
- package/dist/commands/storage/select.js +3 -3
- package/dist/commands/storage/update.js +10 -10
- package/dist/commands/workflows/extend-lease.d.ts +1 -1
- package/dist/commands/workflows/extend-lease.js +2 -3
- package/dist/hooks/prerun/auth.js +2 -1
- package/dist/managers/config-file-manager.d.ts +6 -12
- package/dist/managers/config-file-manager.js +58 -59
- package/dist/utils/progress.js +1 -0
- package/dist/utils/prompt-flags.d.ts +20 -0
- package/dist/utils/prompt-flags.js +121 -0
- package/dist/utils/prompt.service.d.ts +23 -0
- package/dist/utils/prompt.service.js +107 -0
- package/dist/utils/tty.d.ts +1 -0
- package/dist/utils/tty.js +3 -0
- package/oclif.manifest.json +281 -162
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { select } from '@clack/prompts';
|
|
2
1
|
import { Args, Flags } from '@oclif/core';
|
|
3
2
|
import { Orders } from '@super-protocol/sdk-js';
|
|
4
3
|
import { formatEther, parseEther } from 'viem/utils';
|
|
4
|
+
import { promptService } from '../../utils/prompt.service.js';
|
|
5
5
|
import { BaseCommand } from '../base.js';
|
|
6
6
|
export default class WorkflowsExtendLease extends BaseCommand {
|
|
7
7
|
static args = {
|
|
@@ -97,7 +97,6 @@ export default class WorkflowsExtendLease extends BaseCommand {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
async selectPrompt(options) {
|
|
100
|
-
|
|
101
|
-
return this.ensurePromptValue(result);
|
|
100
|
+
return promptService.select(options);
|
|
102
101
|
}
|
|
103
102
|
}
|
|
@@ -2,6 +2,7 @@ import { BaseCommand } from '../../commands/base.js';
|
|
|
2
2
|
import { AppContainer } from '../../lib/container.js';
|
|
3
3
|
import logger from '../../logger.js';
|
|
4
4
|
import { AuthService } from '../../services/auth.service.js';
|
|
5
|
+
import { isInteractiveMode } from '../../utils/tty.js';
|
|
5
6
|
const isBaseCommandClass = (commandClass) => commandClass.prototype instanceof BaseCommand;
|
|
6
7
|
const getConfigFlag = (commandClass, argv) => {
|
|
7
8
|
if (!isBaseCommandClass(commandClass)) {
|
|
@@ -47,7 +48,7 @@ const hook = async (opts) => {
|
|
|
47
48
|
const { configManager, accountManager, providerClient } = container;
|
|
48
49
|
const credentials = await configManager.get('auth');
|
|
49
50
|
if (!credentials?.accessKey) {
|
|
50
|
-
if (!
|
|
51
|
+
if (!isInteractiveMode()) {
|
|
51
52
|
context.error('Authorization not found. Please run: sp account login', { exit: 1 });
|
|
52
53
|
}
|
|
53
54
|
const args = configFile ? ['--config', configFile] : [];
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import { confirm, select } from '@clack/prompts';
|
|
2
|
-
import { ux } from '@oclif/core';
|
|
3
1
|
import type pino from 'pino';
|
|
4
2
|
import { type Account, type CliConfig } from '../config/config.schema.js';
|
|
5
3
|
import type { IManager } from '../interfaces/manager.interface.js';
|
|
6
|
-
type ConfirmPrompt = typeof confirm;
|
|
7
|
-
type SelectPrompt = typeof select;
|
|
8
|
-
type UxLike = Pick<typeof ux, 'error' | 'stdout'>;
|
|
9
4
|
interface ConfigFileManagerOptions {
|
|
10
|
-
confirmPrompt?: ConfirmPrompt;
|
|
11
5
|
runtimeConfigFile?: string;
|
|
12
|
-
selectPrompt?: SelectPrompt;
|
|
13
|
-
ux?: UxLike;
|
|
14
6
|
}
|
|
15
7
|
export declare class ConfigFileManager implements IManager {
|
|
16
8
|
private readonly logger;
|
|
@@ -20,17 +12,19 @@ export declare class ConfigFileManager implements IManager {
|
|
|
20
12
|
private readonly configFilePath;
|
|
21
13
|
private configs;
|
|
22
14
|
private readonly configsDir;
|
|
23
|
-
private readonly confirmPrompt;
|
|
24
15
|
private runtimeConfigFile?;
|
|
25
|
-
private readonly selectPrompt;
|
|
26
|
-
private readonly ux;
|
|
27
16
|
constructor(configDir: string, logger: pino.BaseLogger, options?: ConfigFileManagerOptions);
|
|
28
17
|
createConfig(configFileName: string, name: string, url?: string, account?: Account): Promise<void>;
|
|
18
|
+
updateConfigName(configFileName: string, name: string): Promise<boolean>;
|
|
29
19
|
deleteConfig(configName: string): Promise<void>;
|
|
30
|
-
deleteConfigByName(name?: string
|
|
20
|
+
deleteConfigByName(name?: string): Promise<void>;
|
|
31
21
|
getConfigData(configName: string): CliConfig | undefined;
|
|
32
22
|
getConfigDir(): string;
|
|
33
23
|
getConfigs(): string[];
|
|
24
|
+
getConfigWithName(name: string): {
|
|
25
|
+
file: string;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
34
28
|
getConfigsWithNames(): Array<{
|
|
35
29
|
file: string;
|
|
36
30
|
name: string;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { confirm, isCancel, select } from '@clack/prompts';
|
|
4
|
-
import { ux } from '@oclif/core';
|
|
5
3
|
import { Value } from 'typebox/value';
|
|
6
4
|
import { cliConfigSchema } from '../config/config.schema.js';
|
|
7
5
|
import { DEFAULT_USER_CONFIG_FILE, DEFAULT_USER_CONFIG_NAME, DIR_ACCESS_PERMS, FILE_ACCESS_PERMS, } from '../constants.js';
|
|
@@ -16,24 +14,18 @@ export class ConfigFileManager {
|
|
|
16
14
|
currentConfig: undefined,
|
|
17
15
|
};
|
|
18
16
|
configsDir;
|
|
19
|
-
confirmPrompt;
|
|
20
17
|
runtimeConfigFile;
|
|
21
|
-
selectPrompt;
|
|
22
|
-
ux;
|
|
23
18
|
constructor(configDir, logger, options = {}) {
|
|
24
19
|
this.logger = logger;
|
|
25
20
|
this.configDir = configDir;
|
|
26
21
|
this.configFilePath = path.join(this.configDir, ConfigFileManager.CONFIG_FILE);
|
|
27
22
|
this.configsDir = path.join(this.configDir, ConfigFileManager.CONFIGS_DIR);
|
|
28
|
-
this.confirmPrompt = options.confirmPrompt ?? confirm;
|
|
29
|
-
this.selectPrompt = options.selectPrompt ?? select;
|
|
30
|
-
this.ux = options.ux ?? ux;
|
|
31
23
|
this.runtimeConfigFile = options.runtimeConfigFile;
|
|
32
24
|
}
|
|
33
25
|
async createConfig(configFileName, name, url, account) {
|
|
34
26
|
const configPath = path.join(this.configsDir, configFileName);
|
|
35
27
|
if (fs.existsSync(configPath)) {
|
|
36
|
-
throw new Error(`
|
|
28
|
+
throw new Error(`Account file already exists: ${configFileName}`);
|
|
37
29
|
}
|
|
38
30
|
const config = { name };
|
|
39
31
|
if (url) {
|
|
@@ -49,70 +41,69 @@ export class ConfigFileManager {
|
|
|
49
41
|
});
|
|
50
42
|
this.logger.info({ configFileName, name, url }, 'Created new configuration');
|
|
51
43
|
}
|
|
44
|
+
async updateConfigName(configFileName, name) {
|
|
45
|
+
const configPath = path.join(this.configsDir, configFileName);
|
|
46
|
+
const normalizedName = name.trim();
|
|
47
|
+
if (!normalizedName) {
|
|
48
|
+
throw new Error('Configuration name cannot be empty');
|
|
49
|
+
}
|
|
50
|
+
if (!fs.existsSync(configPath)) {
|
|
51
|
+
throw new Error(`Account file not found: ${configFileName}`);
|
|
52
|
+
}
|
|
53
|
+
let config;
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
56
|
+
config = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
this.logger.warn({ configFileName, err: error }, 'Failed to read config data');
|
|
60
|
+
throw new Error(`Failed to read configuration: ${configFileName}`);
|
|
61
|
+
}
|
|
62
|
+
if (config.name === normalizedName) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
config.name = normalizedName;
|
|
66
|
+
fs.writeFileSync(configPath, JSON.stringify(config, undefined, 2), {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
flag: 'w',
|
|
69
|
+
mode: FILE_ACCESS_PERMS,
|
|
70
|
+
});
|
|
71
|
+
this.logger.info({ configFileName, name: normalizedName }, 'Updated configuration name');
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
52
74
|
async deleteConfig(configName) {
|
|
53
75
|
const configPath = path.join(this.configsDir, configName);
|
|
54
76
|
if (!fs.existsSync(configPath)) {
|
|
55
|
-
throw new Error(`
|
|
77
|
+
throw new Error(`Account file not found: ${configName}`);
|
|
56
78
|
}
|
|
57
79
|
await this.removeConfig(configName);
|
|
58
80
|
fs.unlinkSync(configPath);
|
|
59
81
|
this.logger.info({ configName }, 'Deleted configuration file');
|
|
60
82
|
}
|
|
61
|
-
async deleteConfigByName(name
|
|
83
|
+
async deleteConfigByName(name) {
|
|
62
84
|
const configs = this.getConfigsWithNames();
|
|
63
85
|
if (configs.length === 0) {
|
|
64
|
-
|
|
65
|
-
return;
|
|
86
|
+
throw new Error('No configurations found');
|
|
66
87
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const config = configs.find((c) => c.name === name);
|
|
70
|
-
if (!config) {
|
|
71
|
-
this.ux.error(`Configuration not found: ${name}`);
|
|
72
|
-
}
|
|
73
|
-
configToDelete = config.file;
|
|
88
|
+
if (!name) {
|
|
89
|
+
throw new Error('Configuration name is required');
|
|
74
90
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
options: configs.map((config) => ({
|
|
79
|
-
label: config.name,
|
|
80
|
-
value: config.file,
|
|
81
|
-
})),
|
|
82
|
-
});
|
|
83
|
-
if (isCancel(selection)) {
|
|
84
|
-
this.ux.stdout('Deletion cancelled');
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
configToDelete = selection;
|
|
88
|
-
}
|
|
89
|
-
const configToDeleteData = configs.find((c) => c.file === configToDelete);
|
|
90
|
-
const displayName = configToDeleteData?.name || configToDelete;
|
|
91
|
-
if (!force) {
|
|
92
|
-
const confirmed = await this.confirmPrompt({
|
|
93
|
-
initialValue: false,
|
|
94
|
-
message: `Are you sure you want to delete configuration "${displayName}"?`,
|
|
95
|
-
});
|
|
96
|
-
if (isCancel(confirmed) || !confirmed) {
|
|
97
|
-
this.ux.stdout('Deletion cancelled');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
91
|
+
const configToDeleteData = configs.find((c) => c.name === name || c.file === name);
|
|
92
|
+
if (!configToDeleteData) {
|
|
93
|
+
throw new Error(`Configuration not found: ${name}`);
|
|
100
94
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
this.ux.stdout('No configurations remaining');
|
|
111
|
-
this.ux.stdout('Create a new account with: sp account login --name "Account Name" or sp auth login');
|
|
112
|
-
}
|
|
95
|
+
const configToDelete = configToDeleteData.file;
|
|
96
|
+
const displayName = configToDeleteData.name || configToDelete;
|
|
97
|
+
await this.deleteConfig(configToDelete);
|
|
98
|
+
this.logger.info({ displayName, configToDelete }, 'Successfully deleted configuration');
|
|
99
|
+
const newCurrent = this.getCurrentConfigFile();
|
|
100
|
+
if (newCurrent) {
|
|
101
|
+
const newCurrentConfig = configs.find((c) => c.file === newCurrent);
|
|
102
|
+
this.logger.info({ currentConfig: newCurrentConfig?.name || newCurrent }, 'Current configuration is now');
|
|
113
103
|
}
|
|
114
|
-
|
|
115
|
-
this.
|
|
104
|
+
else {
|
|
105
|
+
this.logger.info('No configurations remaining');
|
|
106
|
+
this.logger.info('Create a new account with: sp account login --name "Account Name" or sp account login');
|
|
116
107
|
}
|
|
117
108
|
}
|
|
118
109
|
getConfigData(configName) {
|
|
@@ -138,6 +129,14 @@ export class ConfigFileManager {
|
|
|
138
129
|
}
|
|
139
130
|
return fs.readdirSync(this.configsDir).filter((file) => file.endsWith('.config.json'));
|
|
140
131
|
}
|
|
132
|
+
getConfigWithName(name) {
|
|
133
|
+
const configs = this.getConfigsWithNames();
|
|
134
|
+
const config = configs.filter((config) => config.name === name || config.file === name).at(0);
|
|
135
|
+
if (!config) {
|
|
136
|
+
throw new Error(`Account with name ${name} not found`);
|
|
137
|
+
}
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
141
140
|
getConfigsWithNames() {
|
|
142
141
|
if (!fs.existsSync(this.configsDir)) {
|
|
143
142
|
return [];
|
package/dist/utils/progress.js
CHANGED
|
@@ -14,6 +14,7 @@ export const createProgressPrinter = ({ action, done = 'completed', start, }) =>
|
|
|
14
14
|
progressBar.stop(`${action} ${lastKey} ${done}`);
|
|
15
15
|
}
|
|
16
16
|
const progressBar = p.progress({ size: 100, style: 'block' });
|
|
17
|
+
lastKey = key;
|
|
17
18
|
progressBar.start(`${start} ${key}`);
|
|
18
19
|
progressBars[key] = progressBar;
|
|
19
20
|
return progressBar;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Args, Flags } from '@oclif/core';
|
|
2
|
+
type FlagDefinition = ReturnType<typeof Flags.string> | ReturnType<typeof Flags.integer> | ReturnType<typeof Flags.boolean>;
|
|
3
|
+
type ArgDefinition = ReturnType<typeof Args.string> | ReturnType<typeof Args.integer> | ReturnType<typeof Args.boolean>;
|
|
4
|
+
export interface MissingFlag {
|
|
5
|
+
name: string;
|
|
6
|
+
definition: FlagDefinition;
|
|
7
|
+
type: 'string' | 'integer' | 'boolean';
|
|
8
|
+
}
|
|
9
|
+
export interface MissingArg {
|
|
10
|
+
name: string;
|
|
11
|
+
definition: ArgDefinition;
|
|
12
|
+
type: 'string' | 'integer' | 'boolean';
|
|
13
|
+
}
|
|
14
|
+
export declare const findMissingRequiredFlags: (flags: Record<string, unknown>, flagDefinitions: Record<string, FlagDefinition> | undefined) => Promise<MissingFlag[]>;
|
|
15
|
+
export declare const findMissingRequiredArgs: (args: Record<string, unknown>, argDefinitions: Record<string, ArgDefinition> | undefined) => Promise<MissingArg[]>;
|
|
16
|
+
export declare const promptForFlag: (missingFlag: MissingFlag) => Promise<string | number | boolean>;
|
|
17
|
+
export declare const promptForArg: (missingArg: MissingArg) => Promise<string | number | boolean>;
|
|
18
|
+
export declare const formatMissingFlagsError: (missingFlags: MissingFlag[]) => string;
|
|
19
|
+
export declare const formatMissingArgsError: (missingArgs: MissingArg[]) => string;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { promptService } from './prompt.service.js';
|
|
2
|
+
const getSampleValue = (definition) => {
|
|
3
|
+
if (typeof definition.min === 'number') {
|
|
4
|
+
return String(definition.min);
|
|
5
|
+
}
|
|
6
|
+
if (typeof definition.max === 'number' && definition.max < 1) {
|
|
7
|
+
return String(definition.max);
|
|
8
|
+
}
|
|
9
|
+
return '1';
|
|
10
|
+
};
|
|
11
|
+
const inferTypeFromDefinition = async (definition, allowNo) => {
|
|
12
|
+
if (definition && typeof definition === 'object') {
|
|
13
|
+
const typedDefinition = definition;
|
|
14
|
+
if (typedDefinition.type === 'boolean' || typedDefinition.type === 'flag') {
|
|
15
|
+
return 'boolean';
|
|
16
|
+
}
|
|
17
|
+
if (typedDefinition.type === 'integer' || typedDefinition.type === 'number') {
|
|
18
|
+
return 'integer';
|
|
19
|
+
}
|
|
20
|
+
if (allowNo && typeof typedDefinition.allowNo === 'boolean') {
|
|
21
|
+
return 'boolean';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const parse = definition?.parse;
|
|
25
|
+
if (typeof parse === 'function') {
|
|
26
|
+
const sample = getSampleValue(definition);
|
|
27
|
+
try {
|
|
28
|
+
const parsed = await parse(sample, {}, definition);
|
|
29
|
+
if (typeof parsed === 'number') {
|
|
30
|
+
return 'integer';
|
|
31
|
+
}
|
|
32
|
+
if (typeof parsed === 'boolean') {
|
|
33
|
+
return 'boolean';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Ignore parsing errors for type inference.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return 'string';
|
|
41
|
+
};
|
|
42
|
+
const parseIntegerValue = (name, input, definition) => {
|
|
43
|
+
if (!/^-?\d+$/.test(input)) {
|
|
44
|
+
throw new Error(`${name} must be a number`);
|
|
45
|
+
}
|
|
46
|
+
const num = Number.parseInt(input, 10);
|
|
47
|
+
if (definition.min !== undefined && num < definition.min) {
|
|
48
|
+
throw new Error(`Expected an integer greater than or equal to ${definition.min} but received: ${input}`);
|
|
49
|
+
}
|
|
50
|
+
if (definition.max !== undefined && num > definition.max) {
|
|
51
|
+
throw new Error(`Expected an integer less than or equal to ${definition.max} but received: ${input}`);
|
|
52
|
+
}
|
|
53
|
+
return num;
|
|
54
|
+
};
|
|
55
|
+
const getFlagType = async (definition) => inferTypeFromDefinition(definition, true);
|
|
56
|
+
export const findMissingRequiredFlags = async (flags, flagDefinitions) => {
|
|
57
|
+
if (!flagDefinitions) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const missing = [];
|
|
61
|
+
for (const [name, definition] of Object.entries(flagDefinitions)) {
|
|
62
|
+
if (definition.required && (flags[name] === undefined || flags[name] === null)) {
|
|
63
|
+
missing.push({
|
|
64
|
+
name,
|
|
65
|
+
definition,
|
|
66
|
+
type: await getFlagType(definition),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return missing;
|
|
71
|
+
};
|
|
72
|
+
const getArgType = async (definition) => inferTypeFromDefinition(definition, false);
|
|
73
|
+
export const findMissingRequiredArgs = async (args, argDefinitions) => {
|
|
74
|
+
if (!argDefinitions) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const missing = [];
|
|
78
|
+
for (const [name, definition] of Object.entries(argDefinitions)) {
|
|
79
|
+
if (definition.required && (args[name] === undefined || args[name] === null)) {
|
|
80
|
+
missing.push({
|
|
81
|
+
name,
|
|
82
|
+
definition,
|
|
83
|
+
type: await getArgType(definition),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return missing;
|
|
88
|
+
};
|
|
89
|
+
export const promptForFlag = async (missingFlag) => {
|
|
90
|
+
const description = missingFlag.definition.description;
|
|
91
|
+
const optionValues = missingFlag.definition.options;
|
|
92
|
+
if (missingFlag.type === 'string' && Array.isArray(optionValues) && optionValues.length > 0) {
|
|
93
|
+
const message = description ? `Select ${description}` : `Select ${missingFlag.name}`;
|
|
94
|
+
return promptService.select({
|
|
95
|
+
message,
|
|
96
|
+
options: optionValues.map((value) => ({
|
|
97
|
+
label: value,
|
|
98
|
+
value,
|
|
99
|
+
})),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const parseValue = missingFlag.type === 'integer'
|
|
103
|
+
? (input) => parseIntegerValue(missingFlag.name, input, missingFlag.definition)
|
|
104
|
+
: undefined;
|
|
105
|
+
return promptService.promptForValue(missingFlag.name, description, missingFlag.type, parseValue);
|
|
106
|
+
};
|
|
107
|
+
export const promptForArg = async (missingArg) => {
|
|
108
|
+
const description = missingArg.definition.description;
|
|
109
|
+
const parseValue = missingArg.type === 'integer'
|
|
110
|
+
? (input) => parseIntegerValue(missingArg.name, input, missingArg.definition)
|
|
111
|
+
: undefined;
|
|
112
|
+
return promptService.promptForValue(missingArg.name, description, missingArg.type, parseValue);
|
|
113
|
+
};
|
|
114
|
+
export const formatMissingFlagsError = (missingFlags) => {
|
|
115
|
+
const flagNames = missingFlags.map((f) => `--${f.name}`).join(', ');
|
|
116
|
+
return `Missing required flags: ${flagNames}. Please provide them or run in interactive mode.`;
|
|
117
|
+
};
|
|
118
|
+
export const formatMissingArgsError = (missingArgs) => {
|
|
119
|
+
const argNames = missingArgs.map((a) => `<${a.name}>`).join(', ');
|
|
120
|
+
return `Missing required arguments: ${argNames}. Please provide them or run in interactive mode.`;
|
|
121
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ConfirmOptions as ClackConfirmOptions, type PasswordOptions as ClackPasswordOptions, type SelectOptions as ClackSelectOptions, type TextOptions as ClackTextOptions } from '@clack/prompts';
|
|
2
|
+
export type TextOptions = ClackTextOptions;
|
|
3
|
+
export type ConfirmOptions = ClackConfirmOptions;
|
|
4
|
+
export type SelectOptions<T> = ClackSelectOptions<T>;
|
|
5
|
+
export type PasswordOptions = ClackPasswordOptions;
|
|
6
|
+
type ParseValue = (input: string) => string | number | boolean;
|
|
7
|
+
export declare class NonInteractiveError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
declare class PromptService {
|
|
11
|
+
private _isInteractive;
|
|
12
|
+
setInteractiveMode(value: boolean): void;
|
|
13
|
+
get isInteractive(): boolean;
|
|
14
|
+
private checkInteractive;
|
|
15
|
+
text(options: ClackTextOptions): Promise<string>;
|
|
16
|
+
confirm(options: ClackConfirmOptions): Promise<boolean>;
|
|
17
|
+
select<T>(options: ClackSelectOptions<T>): Promise<T>;
|
|
18
|
+
password(options: ClackPasswordOptions): Promise<string>;
|
|
19
|
+
ensureValue<T>(value: symbol | T, errorMessage?: string): T;
|
|
20
|
+
promptForValue(name: string, description: string | undefined, type: 'string' | 'integer' | 'boolean', parseValue?: ParseValue): Promise<string | number | boolean>;
|
|
21
|
+
}
|
|
22
|
+
export declare const promptService: PromptService;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { confirm as clackConfirm, password as clackPassword, select as clackSelect, text as clackText, isCancel, } from '@clack/prompts';
|
|
2
|
+
import { isInteractiveMode } from './tty.js';
|
|
3
|
+
export class NonInteractiveError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'NonInteractiveError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
class PromptService {
|
|
10
|
+
_isInteractive = null;
|
|
11
|
+
setInteractiveMode(value) {
|
|
12
|
+
this._isInteractive = value;
|
|
13
|
+
}
|
|
14
|
+
get isInteractive() {
|
|
15
|
+
if (this._isInteractive === null) {
|
|
16
|
+
this._isInteractive = isInteractiveMode();
|
|
17
|
+
}
|
|
18
|
+
return this._isInteractive;
|
|
19
|
+
}
|
|
20
|
+
checkInteractive(operation) {
|
|
21
|
+
if (!this.isInteractive) {
|
|
22
|
+
throw new NonInteractiveError(`Cannot ${operation} in non-interactive mode. Use appropriate flags or run without --no-tty.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async text(options) {
|
|
26
|
+
this.checkInteractive('prompt for text input');
|
|
27
|
+
const result = await clackText(options);
|
|
28
|
+
if (isCancel(result)) {
|
|
29
|
+
throw new Error('Operation cancelled.');
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
async confirm(options) {
|
|
34
|
+
if (!this.isInteractive) {
|
|
35
|
+
if (options.initialValue !== undefined) {
|
|
36
|
+
return options.initialValue;
|
|
37
|
+
}
|
|
38
|
+
throw new NonInteractiveError('Cannot prompt for confirmation in non-interactive mode. Use --force or run without --no-tty.');
|
|
39
|
+
}
|
|
40
|
+
const result = await clackConfirm(options);
|
|
41
|
+
if (isCancel(result)) {
|
|
42
|
+
throw new Error('Operation cancelled.');
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async select(options) {
|
|
47
|
+
this.checkInteractive('show selection menu');
|
|
48
|
+
const result = await clackSelect(options);
|
|
49
|
+
if (isCancel(result)) {
|
|
50
|
+
throw new Error('Operation cancelled.');
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
async password(options) {
|
|
55
|
+
this.checkInteractive('prompt for password');
|
|
56
|
+
const result = await clackPassword(options);
|
|
57
|
+
if (isCancel(result)) {
|
|
58
|
+
throw new Error('Operation cancelled.');
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
ensureValue(value, errorMessage = 'Operation cancelled.') {
|
|
63
|
+
if (isCancel(value)) {
|
|
64
|
+
throw new Error(errorMessage);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
async promptForValue(name, description, type, parseValue) {
|
|
69
|
+
this.checkInteractive('prompt for value');
|
|
70
|
+
const fullDescription = description ? ` (${description})` : '';
|
|
71
|
+
if (type === 'boolean') {
|
|
72
|
+
const message = `Enable ${name}${fullDescription}?`;
|
|
73
|
+
return await this.confirm({ message });
|
|
74
|
+
}
|
|
75
|
+
const message = `Please provide ${name}${fullDescription}:`;
|
|
76
|
+
const parseInput = (value) => {
|
|
77
|
+
if (parseValue) {
|
|
78
|
+
return parseValue(value);
|
|
79
|
+
}
|
|
80
|
+
if (type === 'integer') {
|
|
81
|
+
const num = Number.parseInt(value, 10);
|
|
82
|
+
if (Number.isNaN(num)) {
|
|
83
|
+
throw new Error(`${name} must be a number`);
|
|
84
|
+
}
|
|
85
|
+
return num;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
};
|
|
89
|
+
const result = await this.text({
|
|
90
|
+
message,
|
|
91
|
+
validate: (value) => {
|
|
92
|
+
if (!value || value.trim() === '') {
|
|
93
|
+
return `${name} is required`;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
parseInput(value);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return error instanceof Error ? error.message : String(error);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return parseInput(result);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export const promptService = new PromptService();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isInteractiveMode: (ttyFlag?: boolean) => boolean;
|