@substrio/cli 0.0.0-stage → 0.1.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/LICENSE +177 -0
- package/README.md +66 -2
- package/bin/substrio.mjs +91 -0
- package/package.json +44 -4
- package/src/api.mjs +107 -0
- package/src/apply.mjs +204 -0
- package/src/blobs.mjs +102 -0
- package/src/cli.mjs +405 -0
- package/src/collections.mjs +701 -0
- package/src/command-table.mjs +436 -0
- package/src/commands/bundle.mjs +369 -0
- package/src/commands/count.mjs +39 -0
- package/src/commands/deploy.mjs +488 -0
- package/src/commands/dev.mjs +246 -0
- package/src/commands/env.mjs +115 -0
- package/src/commands/files.mjs +172 -0
- package/src/commands/git.mjs +195 -0
- package/src/commands/groups-runtime.mjs +227 -0
- package/src/commands/groups.mjs +417 -0
- package/src/commands/history.mjs +374 -0
- package/src/commands/import.mjs +506 -0
- package/src/commands/init.mjs +175 -0
- package/src/commands/key.mjs +172 -0
- package/src/commands/login.mjs +167 -0
- package/src/commands/logs.mjs +285 -0
- package/src/commands/mcp-config.mjs +102 -0
- package/src/commands/op.mjs +265 -0
- package/src/commands/open.mjs +35 -0
- package/src/commands/push.mjs +199 -0
- package/src/commands/signup.mjs +164 -0
- package/src/commands/site.mjs +129 -0
- package/src/commands/skills.mjs +177 -0
- package/src/commands/status.mjs +78 -0
- package/src/commands/tenants.mjs +172 -0
- package/src/commands/test.mjs +167 -0
- package/src/commands/tools.mjs +63 -0
- package/src/commands/versions.mjs +293 -0
- package/src/config.mjs +569 -0
- package/src/documents.mjs +139 -0
- package/src/function-build.mjs +273 -0
- package/src/identity.mjs +248 -0
- package/src/jsonfile.mjs +22 -0
- package/src/live-move.mjs +267 -0
- package/src/local/args.mjs +58 -0
- package/src/local/reload.mjs +408 -0
- package/src/local/server.mjs +291 -0
- package/src/local/watch.mjs +175 -0
- package/src/manifest.mjs +591 -0
- package/src/ops.mjs +265 -0
- package/src/readers.mjs +416 -0
- package/src/sdk.mjs +26 -0
- package/src/settings.mjs +117 -0
- package/src/testing/app.mjs +507 -0
- package/src/testing/env.mjs +35 -0
- package/src/testing/index.mjs +11 -0
- package/src/testing/types.mjs +55 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrio CLI: the one front end every command goes through.
|
|
3
|
+
*
|
|
4
|
+
* Global flags are parsed once, wherever they stand on the command line, and
|
|
5
|
+
* taken off before a command sees its arguments:
|
|
6
|
+
* --help, -h print the command's usage and exit 0; nothing runs
|
|
7
|
+
* --json print results as JSON (stdout) and errors as JSON (stderr)
|
|
8
|
+
* --yes, -y confirm a destructive command without asking
|
|
9
|
+
* --profile NAME use the saved profile NAME (as SUBSTRIO_PROFILE)
|
|
10
|
+
* --tenant SLUG talk to tenant SLUG (as SUBSTRIO_TENANT)
|
|
11
|
+
* --dev use the dev server `substrio dev` runs for the project in this directory (its saved profile)
|
|
12
|
+
* --version, -v print the CLI version
|
|
13
|
+
* -- everything after it is the command's own
|
|
14
|
+
*
|
|
15
|
+
* `globals` holds the parsed flags for the commands to read; nothing else
|
|
16
|
+
* parses them.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { writeSync } from 'node:fs';
|
|
20
|
+
import { createInterface } from 'node:readline/promises';
|
|
21
|
+
|
|
22
|
+
const RED = '\x1b[91m';
|
|
23
|
+
const DIM = '\x1b[2m';
|
|
24
|
+
const RESET = '\x1b[0m';
|
|
25
|
+
|
|
26
|
+
const BOOLEAN_FLAGS = new Map([
|
|
27
|
+
['--help', 'help'], ['-h', 'help'], ['--json', 'json'], ['--yes', 'yes'], ['-y', 'yes'],
|
|
28
|
+
['--version', 'version'], ['-v', 'version'], ['--dev', 'dev'],
|
|
29
|
+
]);
|
|
30
|
+
const VALUE_FLAGS = new Map([['--profile', 'profile'], ['--tenant', 'tenant']]);
|
|
31
|
+
|
|
32
|
+
const emptyFlags = () => ({ help: false, json: false, yes: false, version: false, dev: false, profile: null, tenant: null });
|
|
33
|
+
|
|
34
|
+
/** The flags of this run, set once by applyGlobals. */
|
|
35
|
+
export const globals = emptyFlags();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The command, its own arguments and the global flags (pure). `substrio help X`
|
|
39
|
+
* is `substrio X --help`. `--` stops global flag parsing and stays in the command's arguments. Throws when a value flag has no value.
|
|
40
|
+
*/
|
|
41
|
+
export function parseGlobal(argv) {
|
|
42
|
+
const flags = emptyFlags();
|
|
43
|
+
const rest = [];
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
if (arg === '--') {
|
|
47
|
+
// Kept for the command's own splitter (ops.mjs splitPositional), which ends option parsing at it.
|
|
48
|
+
rest.push(...argv.slice(i));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (BOOLEAN_FLAGS.has(arg)) {
|
|
52
|
+
flags[BOOLEAN_FLAGS.get(arg)] = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const eq = arg.indexOf('=');
|
|
56
|
+
const name = arg.startsWith('--') && eq > 0 ? arg.slice(0, eq) : arg;
|
|
57
|
+
if (VALUE_FLAGS.has(name)) {
|
|
58
|
+
const value = name === arg ? argv[++i] : arg.slice(eq + 1);
|
|
59
|
+
if (value === undefined || value === '' || value.startsWith('-')) throw new Error(`${name} needs a value`);
|
|
60
|
+
flags[VALUE_FLAGS.get(name)] = value;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
rest.push(arg);
|
|
64
|
+
}
|
|
65
|
+
if (rest[0] === 'help') {
|
|
66
|
+
flags.help = true;
|
|
67
|
+
rest.shift();
|
|
68
|
+
}
|
|
69
|
+
return { command: rest[0] ?? null, args: rest.slice(1), flags };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Makes `flags` this run's globals; --profile and --tenant reach config as the environment does. */
|
|
73
|
+
export function applyGlobals(flags, env = process.env) {
|
|
74
|
+
Object.assign(globals, flags);
|
|
75
|
+
if (flags.profile) env.SUBSTRIO_PROFILE = flags.profile;
|
|
76
|
+
if (flags.tenant) env.SUBSTRIO_TENANT = flags.tenant;
|
|
77
|
+
if (flags.json) output.machine();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The codes the CLI raises itself, spelled as the server's error codes (the
|
|
82
|
+
* `code` enum of the OpenAPI Error schema). A server error keeps its own.
|
|
83
|
+
*/
|
|
84
|
+
export const ErrorCode = Object.freeze({
|
|
85
|
+
INVALID_ARGUMENT: 'INVALID_ARGUMENT',
|
|
86
|
+
CREDENTIAL_REQUIRED: 'CREDENTIAL_REQUIRED',
|
|
87
|
+
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
|
88
|
+
// The server could not be reached (no HTTP answer at all).
|
|
89
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
90
|
+
// A destructive command ran with no --yes and no terminal to ask on.
|
|
91
|
+
CONFIRMATION_REQUIRED: 'CONFIRMATION_REQUIRED',
|
|
92
|
+
UNKNOWN: 'UNKNOWN',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One line of a server JSON error body into the message people read: its
|
|
97
|
+
* `error` (a string, or an {message} object), falling back to `message`,
|
|
98
|
+
* with `detail` appended; `HTTP status` when the body said nothing (pure).
|
|
99
|
+
* The one owner cli.mjs's own users import; api.mjs keeps its own copy for
|
|
100
|
+
* the callers that already load it for the API client.
|
|
101
|
+
*/
|
|
102
|
+
export function errorText(data, status) {
|
|
103
|
+
const error = data?.error;
|
|
104
|
+
const label = (typeof error === 'string' ? error : error?.message) || data?.message || '';
|
|
105
|
+
const detail = typeof data?.detail === 'string' ? data.detail : '';
|
|
106
|
+
if (detail) return label ? `${label}: ${detail}` : detail;
|
|
107
|
+
return label || `HTTP ${status}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The process exit codes, one per kind of failure, so a script can tell them
|
|
112
|
+
* apart without parsing text. `exitCodeFor` is their one owner; `substrio help`
|
|
113
|
+
* lists them (EXIT_CODE_HELP).
|
|
114
|
+
*/
|
|
115
|
+
export const ExitCode = Object.freeze({
|
|
116
|
+
OK: 0,
|
|
117
|
+
FAILURE: 1, // anything not listed below
|
|
118
|
+
USAGE: 2, // bad arguments or input (INVALID_ARGUMENT, a 400 or 422)
|
|
119
|
+
AUTH: 3, // no credentials or refused (CREDENTIAL_REQUIRED, a 401 or 403)
|
|
120
|
+
NOT_FOUND: 4, // what was named does not exist (a 404 or 410)
|
|
121
|
+
CONFLICT: 5, // it exists already or changed meanwhile (a 409 or 412)
|
|
122
|
+
SERVER: 6, // the server failed (a 5xx)
|
|
123
|
+
NETWORK: 7, // the server could not be reached
|
|
124
|
+
PARTIAL: 8, // a bulk operation stored some documents and refused others (substrio data import)
|
|
125
|
+
CONFIRM: 9, // a destructive command needs --yes (no terminal to ask on, or --json)
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const EXIT_CODE_HELP = [
|
|
129
|
+
[ExitCode.OK, 'success'], [ExitCode.FAILURE, 'failure'], [ExitCode.USAGE, 'usage: bad arguments or input'],
|
|
130
|
+
[ExitCode.AUTH, 'auth: no credentials, or refused'], [ExitCode.NOT_FOUND, 'not found'],
|
|
131
|
+
[ExitCode.CONFLICT, 'conflict: exists already or changed meanwhile'], [ExitCode.SERVER, 'server error (5xx)'],
|
|
132
|
+
[ExitCode.NETWORK, 'network: the server could not be reached'],
|
|
133
|
+
[ExitCode.PARTIAL, 'partial: some documents were stored, others refused (each reported)'],
|
|
134
|
+
[ExitCode.CONFIRM, 'confirm: a destructive command needs --yes (no terminal to ask on, or --json)'],
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// The exit code of a CLI code that came with no HTTP status.
|
|
138
|
+
const EXIT_BY_CODE = new Map([
|
|
139
|
+
[ErrorCode.INVALID_ARGUMENT, ExitCode.USAGE], [ErrorCode.CREDENTIAL_REQUIRED, ExitCode.AUTH],
|
|
140
|
+
[ErrorCode.FILE_NOT_FOUND, ExitCode.NOT_FOUND], [ErrorCode.NETWORK_ERROR, ExitCode.NETWORK],
|
|
141
|
+
[ErrorCode.CONFIRMATION_REQUIRED, ExitCode.CONFIRM],
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
/** The exit code of a CliError: from the server's HTTP status when one came back, else from its code. */
|
|
145
|
+
export function exitCodeFor(error) {
|
|
146
|
+
const status = error.status;
|
|
147
|
+
if (status) {
|
|
148
|
+
if (status === 401 || status === 403) return ExitCode.AUTH;
|
|
149
|
+
if (status === 404 || status === 410) return ExitCode.NOT_FOUND;
|
|
150
|
+
if (status === 409 || status === 412) return ExitCode.CONFLICT;
|
|
151
|
+
if (status >= 500) return ExitCode.SERVER;
|
|
152
|
+
if (status >= 400) return ExitCode.USAGE;
|
|
153
|
+
}
|
|
154
|
+
return EXIT_BY_CODE.get(error.code) ?? ExitCode.FAILURE;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
158
|
+
const plain = (parts) => parts.map(String).join(' ').replace(ANSI, '').replace(/^\s*✗\s*/, '').trim();
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The run's stdout and stderr, and the only writer of both under --json:
|
|
162
|
+
* then stdout holds one JSON document (what `emit` was given, or
|
|
163
|
+
* {"ok": true, "messages": [...]} for a command that only prints text) and
|
|
164
|
+
* stderr holds one error in the server's Error shape ({error, code, status?,
|
|
165
|
+
* detail?, ...}) on a nonzero exit. Text a command prints under --json is
|
|
166
|
+
* kept as messages, never mixed into the JSON.
|
|
167
|
+
*/
|
|
168
|
+
class Output {
|
|
169
|
+
constructor() {
|
|
170
|
+
this.json = false;
|
|
171
|
+
this.emitted = false;
|
|
172
|
+
this.errored = false;
|
|
173
|
+
this.messages = [];
|
|
174
|
+
this.problems = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
machine() {
|
|
178
|
+
if (this.json) return;
|
|
179
|
+
this.json = true;
|
|
180
|
+
console.log = console.info = (...parts) => { const line = plain(parts); if (line) this.messages.push(line); };
|
|
181
|
+
console.error = console.warn = (...parts) => { const line = plain(parts); if (line) this.problems.push(line); };
|
|
182
|
+
process.on('exit', (code) => this.finish(code));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Small documents at exit: written before the process goes.
|
|
186
|
+
write(fd, value) {
|
|
187
|
+
writeSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A result may be large: the stream flushes it before a natural exit.
|
|
191
|
+
emit(data) {
|
|
192
|
+
this.emitted = true;
|
|
193
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
error(body) {
|
|
197
|
+
this.errored = true;
|
|
198
|
+
this.write(2, body);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// One record of a stream (substrio logs --tail): one compact JSON line,
|
|
202
|
+
// written as it comes, so nothing is held until exit.
|
|
203
|
+
line(data) {
|
|
204
|
+
this.emitted = true;
|
|
205
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// A failed run that printed its reason as text still ends in one typed
|
|
209
|
+
// error; one that emitted its result (a status report) already said why.
|
|
210
|
+
finish(code) {
|
|
211
|
+
if (code && !this.errored && !this.emitted) {
|
|
212
|
+
const [first, ...rest] = this.problems.length ? this.problems : this.messages;
|
|
213
|
+
this.error({ error: first || `exit ${code}`, code: ErrorCode.UNKNOWN, ...(rest.length ? { detail: rest.join('\n') } : {}) });
|
|
214
|
+
} else if (!code && !this.emitted) {
|
|
215
|
+
this.write(1, { ok: true, messages: this.messages });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const output = new Output();
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* An error the CLI reports: `code` from ErrorCode (or the server's), `hint`
|
|
224
|
+
* what to do next, `body` a server's error body when one came back.
|
|
225
|
+
*/
|
|
226
|
+
export class CliError extends Error {
|
|
227
|
+
constructor(message, { code = ErrorCode.INVALID_ARGUMENT, hint = null, status = null, body = null } = {}) {
|
|
228
|
+
super(message);
|
|
229
|
+
this.code = code;
|
|
230
|
+
this.hint = hint;
|
|
231
|
+
this.status = status;
|
|
232
|
+
this.body = body;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A failed server call: `message` for people, the server's body and status as they came. */
|
|
236
|
+
static fromResponse(message, body, status) {
|
|
237
|
+
return new CliError(message, { code: body?.code || ErrorCode.UNKNOWN, status, body });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The server's Error shape: a server body as it came (with its status), else built here.
|
|
242
|
+
* `diagnostics` (function.check's shape: severity, message, file, line, column), when a local
|
|
243
|
+
* build failure set it, rides along: a build failure never has a server body, so it always
|
|
244
|
+
* takes the shape built here.
|
|
245
|
+
*/
|
|
246
|
+
toJSON() {
|
|
247
|
+
if (this.body && typeof this.body === 'object' && this.body.error) {
|
|
248
|
+
return { ...this.body, ...(this.status ? { status: this.status } : {}) };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
error: this.message, code: this.code,
|
|
252
|
+
...(this.status ? { status: this.status } : {}), ...(this.hint ? { detail: this.hint } : {}),
|
|
253
|
+
...(this.diagnostics ? { diagnostics: this.diagnostics } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Prints `data` as JSON under --json, else as `human` prints it (pretty JSON when none). */
|
|
259
|
+
export function emit(data, human = null) {
|
|
260
|
+
if (globals.json) output.emit(data);
|
|
261
|
+
else if (human) human(data);
|
|
262
|
+
else console.log(JSON.stringify(data, null, 2));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Prints one record of a stream: a JSON line (NDJSON) under --json, written
|
|
267
|
+
* at once, else as `human` prints it.
|
|
268
|
+
*/
|
|
269
|
+
export function emitLine(data, human) {
|
|
270
|
+
if (globals.json) output.line(data);
|
|
271
|
+
else human(data);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Hands on a key the server just issued (`data` holds `key`): with --print,
|
|
276
|
+
* the key alone on stdout, or under --json the whole response as the one
|
|
277
|
+
* JSON document (the key left out without --print, so it is never shown
|
|
278
|
+
* unasked).
|
|
279
|
+
*/
|
|
280
|
+
export function emitIssuedKey(data, print) {
|
|
281
|
+
if (globals.json) {
|
|
282
|
+
const { key, ...rest } = data;
|
|
283
|
+
output.emit(print ? data : rest);
|
|
284
|
+
} else if (print) {
|
|
285
|
+
process.stdout.write(`${data.key}\n`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reports an error and exits with its ExitCode (exitCodeFor): `err` is a
|
|
291
|
+
* CliError, an Error or a message; `hint` says what to do next. Under
|
|
292
|
+
* --json, the typed error on stderr.
|
|
293
|
+
*/
|
|
294
|
+
export function fail(err, hint = null, code = ErrorCode.INVALID_ARGUMENT) {
|
|
295
|
+
// A CliError keeps its own code and status (so its exit code); the hint
|
|
296
|
+
// fills in when it has none.
|
|
297
|
+
const error = err instanceof CliError ? err : new CliError(err instanceof Error ? err.message : String(err), { code, hint });
|
|
298
|
+
if (hint && !error.hint) error.hint = hint;
|
|
299
|
+
if (globals.json) {
|
|
300
|
+
output.error(error.toJSON());
|
|
301
|
+
} else {
|
|
302
|
+
process.stderr.write(`${RED}✗ ${error.message}${RESET}\n`);
|
|
303
|
+
if (error.hint) process.stderr.write(`${DIM} ${error.hint}${RESET}\n`);
|
|
304
|
+
}
|
|
305
|
+
process.exit(exitCodeFor(error));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Questions on one readline for the whole run. Asking with no terminal on
|
|
310
|
+
* stdin throws (the answer has to come from a flag), so piped input is never
|
|
311
|
+
* read as answers and nothing is half-written.
|
|
312
|
+
*/
|
|
313
|
+
export class Prompter {
|
|
314
|
+
constructor(input = process.stdin, output = process.stdout) {
|
|
315
|
+
this.input = input;
|
|
316
|
+
this.output = output;
|
|
317
|
+
this.rl = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get interactive() {
|
|
321
|
+
return Boolean(this.input.isTTY);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async ask(question, fallback = '', flag = null) {
|
|
325
|
+
if (!this.interactive) {
|
|
326
|
+
if (fallback) return fallback;
|
|
327
|
+
throw new Error(`${question}: no terminal to ask on${flag ? `; pass ${flag}` : ''}`);
|
|
328
|
+
}
|
|
329
|
+
this.rl ??= createInterface({ input: this.input, output: this.output });
|
|
330
|
+
const answer = (await this.rl.question(`${question}${fallback ? ` (${fallback})` : ''}: `)).trim();
|
|
331
|
+
return answer || fallback;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Asks `question` with the answer never echoed to the terminal (a
|
|
336
|
+
* password): raw keystrokes, read and discarded without ever reaching
|
|
337
|
+
* readline's history or the screen. Throws with no terminal, same as ask.
|
|
338
|
+
*/
|
|
339
|
+
async askSecret(question, flag = null) {
|
|
340
|
+
if (!this.interactive) {
|
|
341
|
+
throw new Error(`${question}: no terminal to ask on${flag ? `; pass ${flag}` : ''}`);
|
|
342
|
+
}
|
|
343
|
+
this.output.write(`${question}: `);
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
const stdin = this.input;
|
|
346
|
+
let value = '';
|
|
347
|
+
const onData = (chunk) => {
|
|
348
|
+
for (const ch of chunk.toString('utf8')) {
|
|
349
|
+
if (ch === '\n' || ch === '\r') {
|
|
350
|
+
cleanup();
|
|
351
|
+
this.output.write('\n');
|
|
352
|
+
resolve(value);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (ch === '') { // Ctrl+C
|
|
356
|
+
cleanup();
|
|
357
|
+
this.output.write('\n');
|
|
358
|
+
reject(new Error('cancelled'));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (ch === '' || ch === '\b') value = value.slice(0, -1);
|
|
362
|
+
else value += ch;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const cleanup = () => {
|
|
366
|
+
stdin.setRawMode(false);
|
|
367
|
+
stdin.pause();
|
|
368
|
+
stdin.removeListener('data', onData);
|
|
369
|
+
};
|
|
370
|
+
stdin.setRawMode(true);
|
|
371
|
+
stdin.resume();
|
|
372
|
+
stdin.on('data', onData);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
close() {
|
|
377
|
+
this.rl?.close();
|
|
378
|
+
this.rl = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* The one gate every destructive command runs through (tenants delete,
|
|
384
|
+
* restore, a registry delete/revoke/remove/drop/purge/reset/clear/restore):
|
|
385
|
+
* --yes skips it; a terminal asks through Prompter and refuses on anything
|
|
386
|
+
* but yes; --json or no terminal runs nothing and answers one typed error
|
|
387
|
+
* (code confirmation_required, naming what would be destroyed, `next` the
|
|
388
|
+
* same command with --yes), its own exit code (ExitCode.CONFIRM).
|
|
389
|
+
*/
|
|
390
|
+
export async function confirmDestructive(what, nextCommand) {
|
|
391
|
+
if (globals.yes) return;
|
|
392
|
+
const prompter = new Prompter();
|
|
393
|
+
if (globals.json || !prompter.interactive) {
|
|
394
|
+
fail(new CliError(what, {
|
|
395
|
+
code: ErrorCode.CONFIRMATION_REQUIRED,
|
|
396
|
+
body: { error: what, code: 'confirmation_required', next: nextCommand },
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const answer = await prompter.ask(`${what}. Continue?`, 'n');
|
|
401
|
+
if (!/^y(es)?$/i.test(answer)) fail('Not confirmed.', null, ErrorCode.UNKNOWN);
|
|
402
|
+
} finally {
|
|
403
|
+
prompter.close();
|
|
404
|
+
}
|
|
405
|
+
}
|