insta 0.1.15 → 0.1.16
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 +1 -1
- package/dist/commands/auth.js +3 -3
- package/dist/commands/mcp.js +15 -1
- package/dist/commands/postgres.js +23 -0
- package/dist/config.js +6 -4
- package/dist/index.js +5 -2
- package/dist/observe/hook.js +9 -3
- package/dist/observe/scanner.js +24 -14
- package/dist/util.js +34 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ build never reaches a production installer.
|
|
|
231
231
|
| `insta secrets` | Write `.env`, plus `list` · `set` · `unset` · `bind` · `unbind` · `bindings` · `sources` · `tree` |
|
|
232
232
|
| `insta domain` | Bought or bring-your-own: `attach` · `check` · `detach`; buy through InstaCloud: `search` · `buy` · `list` · `status` · `records …` |
|
|
233
233
|
| `insta compute` | `start` · `stop` · `suspend` · `restart` · `status` · `scale` · `limits` · `volume` · `always-on` · `exec` · `ssh` · `repo` · `connect-repo` · `watch-paths` · `disconnect-repo` · `logs` · `metrics` |
|
|
234
|
-
| `insta postgres` | `url` (print the DSN) · `connect` (psql) · `stats` · `limits` · `volume` · `always-on` · `private-access` · `public-access` · `logs` · `metrics` — every verb takes `[service]` |
|
|
234
|
+
| `insta postgres` | `url` (print the DSN) · `connect` (psql) · `stats` · `limits` · `restart` · `volume` · `always-on` · `private-access` · `public-access` · `logs` · `metrics` — every verb takes `[service]` |
|
|
235
235
|
| `insta redis` · `mysql` · `mongodb` | `query` · `status` · `limits` · `volume` · `always-on` · `logs` · `metrics` |
|
|
236
236
|
| `insta storage` | `list` · `get` · `delete` · `set-access` |
|
|
237
237
|
| `insta build [dir]` · `deploy [dir]` | Verify a source dir would build; deploy a source directory (built remotely) or `--image <url>`. `insta build logs <id>` reads source-build output: `--source archive` (default) uses the deploy operation ID, `--source github` uses a GitHub build ID; `--follow` watches output, `--json` returns one snapshot |
|
package/dist/commands/auth.js
CHANGED
|
@@ -325,7 +325,7 @@ export async function claimGrant(email, client, post, wait = sleepSeconds, open,
|
|
|
325
325
|
throw expired();
|
|
326
326
|
}
|
|
327
327
|
// Start a loopback server, open the browser at the platform bridge, and await the token.
|
|
328
|
-
function browserOauth(apiUrl, provider) {
|
|
328
|
+
export function browserOauth(apiUrl, provider, open = openUrl) {
|
|
329
329
|
return new Promise((resolve, reject) => {
|
|
330
330
|
const state = randomBytes(16).toString('hex');
|
|
331
331
|
let timer;
|
|
@@ -340,7 +340,7 @@ function browserOauth(apiUrl, provider) {
|
|
|
340
340
|
const err = url.searchParams.get('error');
|
|
341
341
|
const ok = !!token && !err && url.searchParams.get('state') === state;
|
|
342
342
|
res.writeHead(ok ? 200 : 400, { 'content-type': 'text/html' });
|
|
343
|
-
res.end(`<!doctype html><meta charset=utf-8><body style="font-family:system-ui;text-align:center;margin-top:4rem"><h2>InstaCloud</h2><p>${ok ? '✓ Login complete — you can close this tab.' : '✗ Login failed' + (err ? ` (${err})` : '')}</p></body>`);
|
|
343
|
+
res.end(`<!doctype html><meta charset=utf-8><body style="font-family:system-ui;text-align:center;margin-top:4rem"><h2>InstaCloud</h2><p>${ok ? '✓ Login complete — you can close this tab.' : '✗ Login failed' + (err ? ` (${err.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')})` : '')}</p></body>`);
|
|
344
344
|
clearTimeout(timer);
|
|
345
345
|
server.close();
|
|
346
346
|
if (err)
|
|
@@ -362,7 +362,7 @@ function browserOauth(apiUrl, provider) {
|
|
|
362
362
|
// event, so openUrl's return value cannot see it (e.g. powershell.exe blocked by AppLocker
|
|
363
363
|
// on hardened fleets) — and the silent variant of that failure looks exactly like a hang.
|
|
364
364
|
info(`if nothing opens, use this URL:\n ${authorizeUrl}`);
|
|
365
|
-
|
|
365
|
+
open(authorizeUrl);
|
|
366
366
|
info('waiting for you to finish in the browser… (times out in 2m; ctrl-c to abort)');
|
|
367
367
|
timer = setTimeout(() => { server.close(); reject(new Error('timed out waiting for browser login (2m)')); }, 120_000);
|
|
368
368
|
});
|
package/dist/commands/mcp.js
CHANGED
|
@@ -51,6 +51,20 @@ export function renderJsonConfig(slug, existing, url, name = MCP_SERVER_NAME) {
|
|
|
51
51
|
}
|
|
52
52
|
return JSON.stringify(root, null, 2) + '\n';
|
|
53
53
|
}
|
|
54
|
+
export function tomlBasicString(value) {
|
|
55
|
+
const escaped = value.replace(/[\u0000-\u0008\u000A-\u001F\u007F\\"]/g, (ch) => {
|
|
56
|
+
switch (ch) {
|
|
57
|
+
case '\\': return '\\\\';
|
|
58
|
+
case '"': return '\\"';
|
|
59
|
+
case '\n': return '\\n';
|
|
60
|
+
case '\r': return '\\r';
|
|
61
|
+
case '\b': return '\\b';
|
|
62
|
+
case '\f': return '\\f';
|
|
63
|
+
default: return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return `"${escaped}"`;
|
|
67
|
+
}
|
|
54
68
|
// Codex config is TOML. Appending a complete `[mcp_servers.<name>]` table is always valid at
|
|
55
69
|
// EOF, so we avoid a TOML parser: string-detect for idempotency, append for install.
|
|
56
70
|
export function renderCodexConfig(existing, url, name = MCP_SERVER_NAME) {
|
|
@@ -58,7 +72,7 @@ export function renderCodexConfig(existing, url, name = MCP_SERVER_NAME) {
|
|
|
58
72
|
if (base.includes(`[mcp_servers.${name}]`))
|
|
59
73
|
return null; // already configured
|
|
60
74
|
const sep = base.length && !base.endsWith('\n') ? '\n' : '';
|
|
61
|
-
return `${base}${sep}\n[mcp_servers.${name}]\nurl =
|
|
75
|
+
return `${base}${sep}\n[mcp_servers.${name}]\nurl = ${tomlBasicString(url)}\n`;
|
|
62
76
|
}
|
|
63
77
|
// Install for one agent. Returns 'installed' | 'already' | 'skipped' (unparseable config).
|
|
64
78
|
export async function installFor(slug, home, url, name = MCP_SERVER_NAME) {
|
|
@@ -282,6 +282,29 @@ export async function dbVolume(service, opts) {
|
|
|
282
282
|
const vg = res.body?.volumeGib;
|
|
283
283
|
info(`postgres ${service ?? 'default'}: volume ${typeof vg === 'number' ? `grown to ${vg}Gi` : `set to ${sizeGib}Gi`}`);
|
|
284
284
|
}
|
|
285
|
+
export async function dbRestart(service, opts) {
|
|
286
|
+
const api = await ApiClient.load();
|
|
287
|
+
const p = await requireProject();
|
|
288
|
+
const qs = new URLSearchParams();
|
|
289
|
+
const branch = opts.branch ?? p.branch;
|
|
290
|
+
if (branch)
|
|
291
|
+
qs.set('branch', branch);
|
|
292
|
+
if (service)
|
|
293
|
+
qs.set('group', service);
|
|
294
|
+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/database/restart${qs.toString() ? `?${qs}` : ''}`);
|
|
295
|
+
if (handleApproval(res, opts.json))
|
|
296
|
+
return;
|
|
297
|
+
if (opts.json)
|
|
298
|
+
return printJson(res.body);
|
|
299
|
+
info(restartLine(service ?? 'default', res.body));
|
|
300
|
+
}
|
|
301
|
+
export function restartLine(group, body) {
|
|
302
|
+
if (body?.pending)
|
|
303
|
+
return `postgres ${group}: restart accepted and still in progress — settings apply once it is back up`;
|
|
304
|
+
if (body?.state === 'suspended')
|
|
305
|
+
return `postgres ${group}: suspended, not restarted — pending settings apply when the next connection starts it`;
|
|
306
|
+
return `postgres ${group}: restarted — ALTER SYSTEM settings that need a restart are now in effect`;
|
|
307
|
+
}
|
|
285
308
|
export async function resolveDbUrl(api, projectId, branch, service, json) {
|
|
286
309
|
const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
|
|
287
310
|
const svc = resolveSoleService(services, 'postgres', service);
|
package/dist/config.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
|
-
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { chmod, lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { ensureGitignore } from './gitignore.js';
|
|
7
|
-
import { die } from './util.js';
|
|
7
|
+
import { die, writeFileAtomicSync } from './util.js';
|
|
8
8
|
import { DEFAULT_ENV, ENVS, envForApiUrl, envFromEnvVar, normalizeUrl } from './env.js';
|
|
9
9
|
const GLOBAL_DIR = join(homedir(), '.insta');
|
|
10
10
|
const GLOBAL_FILE = join(GLOBAL_DIR, 'config.json');
|
|
@@ -109,8 +109,10 @@ export async function readPersistedGlobal() {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
export async function writeGlobal(c) {
|
|
112
|
-
await mkdir(GLOBAL_DIR, { recursive: true });
|
|
113
|
-
|
|
112
|
+
await mkdir(GLOBAL_DIR, { recursive: true, mode: 0o700 });
|
|
113
|
+
if (process.platform !== 'win32' && !(await lstat(GLOBAL_DIR)).isSymbolicLink())
|
|
114
|
+
await chmod(GLOBAL_DIR, 0o700);
|
|
115
|
+
writeFileAtomicSync(GLOBAL_FILE, JSON.stringify(c, null, 2));
|
|
114
116
|
}
|
|
115
117
|
/** The home directory is never a project root. `~/.insta/` is this CLI's GLOBAL config directory,
|
|
116
118
|
* so a project.json there is not a project link: honouring one made every directory under the home
|
package/dist/index.js
CHANGED
|
@@ -365,7 +365,7 @@ compute.command('start [service]').description('Bring a compute service online (
|
|
|
365
365
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)));
|
|
366
366
|
compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
|
|
367
367
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStop(service, o)));
|
|
368
|
-
compute.command('suspend [service]').description('Suspend a compute service (RAM snapshot);
|
|
368
|
+
compute.command('suspend [service]').description('Suspend a compute service (RAM snapshot); traffic can wake it unless already stopped; `start` clears the stop')
|
|
369
369
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeSuspend(service, o)));
|
|
370
370
|
compute.command('restart [service]').description("Restart a compute service by re-running the image it already runs against a freshly resolved env bundle — this is how a changed secret or binding reaches a running machine (env is baked into the machine at deploy time), and how a machine that is up but wedged gets cycled (`start` no-ops on one that is already started). No new image, no new spec. The service must be running: a stopped or suspended one comes back with `insta compute start`. All plans; gated: deploy — it lands configuration the same way a deploy does, so a policy denying deploys denies this too (`start`/`stop` stay ungated, and cycle a wedged machine without one). A service whose app fails to answer on its port coming back up reports that failure, and the machines are rolled back, best-effort, to the config they were serving")
|
|
371
371
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeRestart(service, o)));
|
|
@@ -423,7 +423,7 @@ compute.command('volume [service]').description("Show, attach, grow, remount, or
|
|
|
423
423
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeVolume(service, o)));
|
|
424
424
|
addObservability(compute, 'compute', 'compute');
|
|
425
425
|
// ---- postgres ----
|
|
426
|
-
const pg = program.command('postgres').description('Postgres services: connection string, psql, stats, resource ceiling, volume, always-on, network access, logs, metrics');
|
|
426
|
+
const pg = program.command('postgres').description('Postgres services: connection string, psql, stats, resource ceiling, restart, volume, always-on, network access, logs, metrics');
|
|
427
427
|
pg.command('url [service]').description('Print the postgres connection string (DSN) — bare on stdout for piping, e.g. `psql "$(insta postgres url)"` (gated: secrets.read). `insta secrets` includes the primary postgres service\'s DATABASE_URL; use this command to select a specific postgres service')
|
|
428
428
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
429
429
|
.action(guard((service, o) => pgCmd.dbUrl(service, o)));
|
|
@@ -437,6 +437,9 @@ pg.command('limits [service]').description("Show or set a postgres service's res
|
|
|
437
437
|
.option('--cpu <n>', 'vCPU ceiling, e.g. 2 or 2500m').option('--memory <size>', 'memory ceiling, e.g. 4Gi')
|
|
438
438
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
439
439
|
.action(guard((service, o) => pgCmd.dbLimits(service, o)));
|
|
440
|
+
pg.command('restart [service]').description("Restart the postgres server so settings written with ALTER SYSTEM that need a restart (wal_level, max_wal_senders, max_replication_slots, …) take effect. Connections drop while it restarts; waits for it to finish. A suspended instance stays suspended and picks the settings up on its next connection. insta-db-backed only (gated: deploy)")
|
|
441
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
442
|
+
.action(guard((service, o) => pgCmd.dbRestart(service, o)));
|
|
440
443
|
pg.command('volume [service]').description("Show or grow a postgres service's provisioned volume (block disk; insta-db-backed only). No --size: print size and the plan cap (any plan). --size grows it (paid plans; grow-only — a provisioned disk cannot shrink). Billing is actual data stored — the size is a cap, not a price")
|
|
441
444
|
.option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
|
|
442
445
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
package/dist/observe/hook.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// PostToolUse hook: reads a tool-use event on stdin (Claude Code / Codex), scans every string
|
|
2
2
|
// surface for credential exposure, and appends findings to ./.insta/audit.jsonl.
|
|
3
|
-
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, chmodSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { scanEvent } from './scanner.js';
|
|
@@ -27,9 +27,15 @@ export function recordFindings(event, baseDir) {
|
|
|
27
27
|
tool: event.tool_name ?? null,
|
|
28
28
|
cwd: event.cwd ?? null,
|
|
29
29
|
};
|
|
30
|
-
mkdirSync(instaDir, { recursive: true });
|
|
30
|
+
mkdirSync(instaDir, { recursive: true, mode: 0o700 });
|
|
31
|
+
// Windows ACLs do not store these modes. On POSIX a failure must surface: main() prints it.
|
|
32
|
+
if (process.platform !== 'win32')
|
|
33
|
+
chmodSync(instaDir, 0o700);
|
|
31
34
|
const lines = findings.map((f) => JSON.stringify({ ...common, ...f })).join('\n') + '\n';
|
|
32
|
-
|
|
35
|
+
const auditPath = join(instaDir, 'audit.jsonl');
|
|
36
|
+
appendFileSync(auditPath, lines, { encoding: 'utf8', mode: 0o600 });
|
|
37
|
+
if (process.platform !== 'win32')
|
|
38
|
+
chmodSync(auditPath, 0o600);
|
|
33
39
|
return findings.length;
|
|
34
40
|
}
|
|
35
41
|
async function readStdin() {
|
package/dist/observe/scanner.js
CHANGED
|
@@ -8,7 +8,7 @@ const DETECTORS = [
|
|
|
8
8
|
{ name: 'stripe_secret_key', rx: /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}\b/gd, group: false },
|
|
9
9
|
{ name: 'llm_api_key', rx: /\bsk-(?:ant-)?[A-Za-z0-9_-]{20,}\b/gd, group: false },
|
|
10
10
|
{ name: 'google_api_key', rx: /\bAIza[0-9A-Za-z_-]{35}\b/gd, group: false },
|
|
11
|
-
{ name: 'private_key_block', rx: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY
|
|
11
|
+
{ name: 'private_key_block', rx: /-----BEGIN ((?:[A-Z ]+ )?PRIVATE KEY)-----(?:(?:[ \t\r\n]|\\[rn])*(?:Proc-Type|DEK-Info):[^\r\n\\]*(?:\r?\n|\\[rn]|$))*(?:[A-Za-z0-9+/=\r\n \t]|\\[rn])*(?:-----END \1-----)?/gd, group: false },
|
|
12
12
|
{ name: 'jwt', rx: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gd, group: false },
|
|
13
13
|
{ name: 'db_conn_string', rx: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:\s/@]+:[^@\s/]+@[^\s'"]+/gd, group: false },
|
|
14
14
|
{ name: 'bearer_token', rx: /\bbearer\s+([A-Za-z0-9._-]{20,})/gid, group: true },
|
|
@@ -41,17 +41,13 @@ function fingerprint(secret, detector) {
|
|
|
41
41
|
const tail = last4 ? '••••' + last4 : '••••';
|
|
42
42
|
return `${detector}:${tail}:#${h}`;
|
|
43
43
|
}
|
|
44
|
-
function snippet(text,
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const i = redacted.indexOf(marker);
|
|
49
|
-
const start = Math.max(0, i - 36);
|
|
50
|
-
const end = Math.min(redacted.length, i + marker.length + 36);
|
|
51
|
-
let out = redacted.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
44
|
+
function snippet(text, offset, fp) {
|
|
45
|
+
const start = Math.max(0, offset - 36);
|
|
46
|
+
const end = Math.min(text.length, offset + fp.length + 2 + 36);
|
|
47
|
+
let out = text.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
52
48
|
if (start > 0)
|
|
53
49
|
out = '…' + out;
|
|
54
|
-
if (end <
|
|
50
|
+
if (end < text.length)
|
|
55
51
|
out = out + '…';
|
|
56
52
|
return out.slice(0, 160);
|
|
57
53
|
}
|
|
@@ -104,8 +100,12 @@ function scanText(text) {
|
|
|
104
100
|
hits.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // stable: earlier detector wins an equal span
|
|
105
101
|
const kept = [];
|
|
106
102
|
for (const r of hits) {
|
|
107
|
-
|
|
103
|
+
const previous = kept.at(-1);
|
|
104
|
+
if (previous && r[0] < previous[1]) {
|
|
105
|
+
previous[1] = Math.max(previous[1], r[1]);
|
|
106
|
+
previous[3] = text.slice(previous[0], previous[1]);
|
|
108
107
|
continue;
|
|
108
|
+
}
|
|
109
109
|
kept.push(r);
|
|
110
110
|
}
|
|
111
111
|
return kept;
|
|
@@ -178,10 +178,20 @@ export function scanEvent(event, opts = {}) {
|
|
|
178
178
|
if (!text || text.length > 1_000_000)
|
|
179
179
|
continue;
|
|
180
180
|
const side = surface.startsWith('input') ? 'input' : 'output';
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
let redacted = '';
|
|
182
|
+
let cursor = 0;
|
|
183
|
+
const hits = scanText(text).map(([start, end, detector, secret]) => {
|
|
184
|
+
const fp = fingerprint(secret.trimEnd(), detector);
|
|
185
|
+
redacted += text.slice(cursor, start);
|
|
186
|
+
const offset = redacted.length;
|
|
187
|
+
redacted += `«${fp}»`;
|
|
188
|
+
cursor = end;
|
|
189
|
+
return { detector, fp, offset };
|
|
190
|
+
});
|
|
191
|
+
redacted += text.slice(cursor);
|
|
192
|
+
for (const { detector, fp, offset } of hits) {
|
|
183
193
|
const [kind, sev, sink, note] = classify(tool, side, command, filePath, secretFileTarget);
|
|
184
|
-
add(kind, sev, detector, `${tool}:${surface}`, sink, fp, snippet(
|
|
194
|
+
add(kind, sev, detector, `${tool}:${surface}`, sink, fp, snippet(redacted, offset, fp), note);
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
197
|
return findings;
|
package/dist/util.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Output + small pure helpers (env serialization is unit-tested).
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
+
import { Writable } from 'node:stream';
|
|
3
4
|
import { spawn } from 'node:child_process';
|
|
4
5
|
import { chmodSync, copyFileSync, existsSync, lstatSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -253,17 +254,39 @@ export function serializeEnv(bundle) {
|
|
|
253
254
|
.map(([k, v]) => `${k}="${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`)
|
|
254
255
|
.join('\n') + '\n');
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
export function mutePasswordOutput(real) {
|
|
258
|
+
return new Writable({
|
|
259
|
+
write(chunk, _encoding, callback) {
|
|
260
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
|
|
261
|
+
const endings = text.replace(/[^\r\n]/g, '');
|
|
262
|
+
if (endings.length)
|
|
263
|
+
real.write(endings);
|
|
264
|
+
callback();
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export function promptPasswordFrom(input, output, label = 'Password: ') {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
let value = '';
|
|
271
|
+
const rl = createInterface({
|
|
272
|
+
input,
|
|
273
|
+
output: mutePasswordOutput(output),
|
|
274
|
+
// TTY stdin needs raw mode even when stdout is redirected, or the terminal echoes secrets.
|
|
275
|
+
terminal: Boolean(input.isTTY),
|
|
276
|
+
});
|
|
277
|
+
output.write(label);
|
|
278
|
+
rl.on('SIGINT', () => {
|
|
279
|
+
reject(new CliCancel());
|
|
280
|
+
rl.close();
|
|
281
|
+
});
|
|
282
|
+
rl.on('line', (line) => {
|
|
283
|
+
value = line;
|
|
284
|
+
rl.close();
|
|
285
|
+
});
|
|
286
|
+
rl.on('close', () => resolve(value));
|
|
267
287
|
});
|
|
268
288
|
}
|
|
289
|
+
export function promptPassword(label = 'Password: ') {
|
|
290
|
+
return promptPasswordFrom(process.stdin, process.stdout, label);
|
|
291
|
+
}
|
|
269
292
|
//# sourceMappingURL=util.js.map
|