hop2 0.11.45 → 0.11.46
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/hop +108 -41
- package/lib/trust-dialog.js +65 -0
- package/package.json +1 -1
- package/tests/trust-dialog.unit.test.cjs +45 -0
package/hop
CHANGED
|
@@ -2650,21 +2650,8 @@ const CLAUDE_UI_RE = /bypass permissions on|shift\+tab to cycle|Welcome back|esc
|
|
|
2650
2650
|
// sat at the dialog for two weeks (lightscope, Neptune). Whitespace-
|
|
2651
2651
|
// tolerant because the grid text can run words together across wraps.
|
|
2652
2652
|
const TRUST_PROMPT_RE = /Quick\s*safety\s*check|Yes,?\s*I\s*trust\s*this\s*folder|Is\s*this\s*a\s*project\s*you\s*created\s*or\s*one\s*you\s*trust/i;
|
|
2653
|
-
//
|
|
2654
|
-
|
|
2655
|
-
// shell happened to wander.
|
|
2656
|
-
function trustDialogWorkspace(screenText) {
|
|
2657
|
-
const m = /Accessing\s*workspace:?\s*(\/[^\s\u2502\u2500]+)/i.exec(String(screenText || ''));
|
|
2658
|
-
return m ? m[1].replace(/\/+$/, '') : null;
|
|
2659
|
-
}
|
|
2660
|
-
function samePath(a, b) {
|
|
2661
|
-
const norm = (p) => {
|
|
2662
|
-
let v = String(p || '').replace(/\/+$/, '');
|
|
2663
|
-
try { v = fs.realpathSync(v); } catch (e) { /* keep as-is */ }
|
|
2664
|
-
return v;
|
|
2665
|
-
};
|
|
2666
|
-
return !!a && !!b && norm(a) === norm(b);
|
|
2667
|
-
}
|
|
2653
|
+
// Trust-dialog reading lives in lib/trust-dialog.js so it can be unit-tested.
|
|
2654
|
+
const { trustDialogWorkspace, trustDialogAnswer, samePath } = require('./lib/trust-dialog.js');
|
|
2668
2655
|
function getResumeMode() {
|
|
2669
2656
|
const mode = String(getHopConfig().resume_mode || '').toLowerCase();
|
|
2670
2657
|
return mode === 'summary' || mode === 'ask' ? mode : 'full';
|
|
@@ -6109,7 +6096,34 @@ async function sweepRestoreReconcile() {
|
|
|
6109
6096
|
// The screen and foreground name lie for argv-launched sessions.
|
|
6110
6097
|
const procUp = claudeProcessRunning(exp.sessionId);
|
|
6111
6098
|
const running = !isShellProcName(entry.foregroundProcess);
|
|
6112
|
-
const
|
|
6099
|
+
const alive = procUp || running;
|
|
6100
|
+
// A trust dialog is only a dialog while claude is up to show it.
|
|
6101
|
+
// Checked BEFORE "converged": a live claude parked at the dialog
|
|
6102
|
+
// used to be declared running and left there forever, while the
|
|
6103
|
+
// dialog's text lingering on a dead session's screen — the
|
|
6104
|
+
// process gone, a shell prompt under it — was "answered" every
|
|
6105
|
+
// tick, typing digits into the shell (motif, 2026-09-10).
|
|
6106
|
+
const settledMs = now - Math.max(exp.lastDriveAt || 0, exp.recordedAt || 0);
|
|
6107
|
+
if (TRUST_PROMPT_RE.test(text) && alive && settledMs >= 6000) {
|
|
6108
|
+
const trust = trustDialogAnswer(text, exp.cwd);
|
|
6109
|
+
if (trust.ours) {
|
|
6110
|
+
if (!exp.trustAnsweredAt || now - exp.trustAnsweredAt > 20_000) {
|
|
6111
|
+
try {
|
|
6112
|
+
const e2 = await attachTerminalApiClient(name, exp.displayName, { actor: 'system', label: 'reconcile' });
|
|
6113
|
+
try { sendTerminalInput(e2, trust.input); } finally { try { closeTerminalApiEntry(e2.id); } catch (e) {} }
|
|
6114
|
+
exp.trustAnsweredAt = now;
|
|
6115
|
+
changed = true;
|
|
6116
|
+
log(`[reconcile] ${exp.displayName}: accepted ${trust.preapproval ? 'pre-approved permissions' : 'workspace trust'} (${trust.how})`, DIM);
|
|
6117
|
+
} catch (e) { /* try again next tick */ }
|
|
6118
|
+
}
|
|
6119
|
+
continue;
|
|
6120
|
+
}
|
|
6121
|
+
// Trust dialog for some OTHER directory — not ours to answer.
|
|
6122
|
+
exp.status = 'needs-attention';
|
|
6123
|
+
exp.reason = `claude is asking to trust ${trust.asked || 'a directory'}, not ${exp.cwd}`;
|
|
6124
|
+
changed = true;
|
|
6125
|
+
continue;
|
|
6126
|
+
const claudeUp = alive || CLAUDE_UI_RE.test(text);
|
|
6113
6127
|
if (claudeUp) {
|
|
6114
6128
|
exp.status = 'converged';
|
|
6115
6129
|
exp.convergedAt = now;
|
|
@@ -6117,23 +6131,6 @@ async function sweepRestoreReconcile() {
|
|
|
6117
6131
|
log(`[reconcile] ${exp.displayName}: verified running`, DIM);
|
|
6118
6132
|
continue;
|
|
6119
6133
|
}
|
|
6120
|
-
// A dialog: the resume sweep answers resume prompts already; the
|
|
6121
|
-
// trust dialog we answer here (only for this session's own cwd).
|
|
6122
|
-
if (TRUST_PROMPT_RE.test(text)) {
|
|
6123
|
-
const asked = trustDialogWorkspace(text);
|
|
6124
|
-
if (asked && samePath(asked, exp.cwd)) {
|
|
6125
|
-
try {
|
|
6126
|
-
const e2 = await attachTerminalApiClient(name, exp.displayName, { actor: 'system', label: 'reconcile' });
|
|
6127
|
-
try { sendTerminalInput(e2, '1\r'); } finally { try { closeTerminalApiEntry(e2.id); } catch (e) {} }
|
|
6128
|
-
log(`[reconcile] ${exp.displayName}: accepted workspace trust`, DIM);
|
|
6129
|
-
} catch (e) { /* try again next tick */ }
|
|
6130
|
-
continue;
|
|
6131
|
-
}
|
|
6132
|
-
// Trust dialog for some OTHER directory — not ours to answer.
|
|
6133
|
-
exp.status = 'needs-attention';
|
|
6134
|
-
exp.reason = `claude is asking to trust ${asked || 'a directory'}, not ${exp.cwd}`;
|
|
6135
|
-
changed = true;
|
|
6136
|
-
continue;
|
|
6137
6134
|
}
|
|
6138
6135
|
if (RESUME_PROMPT_RE.test(text)) continue; // resume sweep owns this
|
|
6139
6136
|
// A busy non-shell foreground we did not recognise as claude: the
|
|
@@ -6560,18 +6557,32 @@ async function restoreSavedSessionsUnlocked({ dryRun = false, includeAgents = fa
|
|
|
6560
6557
|
const grid = await getRoomGridPreviewShared(job.plan.internalName);
|
|
6561
6558
|
screen = String(grid?.text || '');
|
|
6562
6559
|
} catch (e) { continue; }
|
|
6563
|
-
|
|
6560
|
+
// Only a LIVE dialog: the same text lingers on screen after
|
|
6561
|
+
// claude has exited (or before the new one has drawn), and
|
|
6562
|
+
// answering it then types into a shell or into a claude that
|
|
6563
|
+
// has not asked yet.
|
|
6564
|
+
// ...and only once the fresh claude has had time to DRAW it:
|
|
6565
|
+
// the previous claude's dialog text is still on screen for the
|
|
6566
|
+
// first seconds after launch, and an answer sent then reaches
|
|
6567
|
+
// a claude that has not asked yet (the Enter lands on whatever
|
|
6568
|
+
// it shows next — for this dialog, the default "No, exit").
|
|
6569
|
+
const dialogLive = TRUST_PROMPT_RE.test(screen) && Date.now() - job.since >= 6000 && (() => {
|
|
6570
|
+
const recId = readClaudeSessionRecord(job.plan.internalName)?.sessionId;
|
|
6571
|
+
const fg = getRuntimeSessionMetadata(job.plan.internalName)?.foregroundProcess;
|
|
6572
|
+
return (!!recId && claudeProcessRunning(recId)) || (!!fg && !isShellProcName(fg));
|
|
6573
|
+
})();
|
|
6574
|
+
if (dialogLive && !job.trustAnswered) {
|
|
6564
6575
|
// Answer only for the workspace this session was launched
|
|
6565
6576
|
// in. A dialog naming any other directory is not ours to
|
|
6566
6577
|
// wave through - leave it on screen and say so.
|
|
6567
|
-
const
|
|
6568
|
-
if (
|
|
6569
|
-
try { sendTerminalInput(job.entry,
|
|
6570
|
-
log(` ${getSessionDisplayName(job.plan.internalName) || job.plan.internalName}: accepted workspace trust for ${asked}`, DIM);
|
|
6578
|
+
const trust = trustDialogAnswer(screen, job.plan.cwd);
|
|
6579
|
+
if (trust.ours) {
|
|
6580
|
+
try { sendTerminalInput(job.entry, trust.input); } catch (e) { finish('write-failed'); continue; }
|
|
6581
|
+
log(` ${getSessionDisplayName(job.plan.internalName) || job.plan.internalName}: accepted ${trust.preapproval ? 'pre-approved permissions' : `workspace trust for ${trust.asked}`} (${trust.how})`, DIM);
|
|
6571
6582
|
job.trustAnswered = true;
|
|
6572
6583
|
job.since = Date.now();
|
|
6573
6584
|
} else {
|
|
6574
|
-
log(` ${getSessionDisplayName(job.plan.internalName) || job.plan.internalName}: claude is asking to trust ${asked || 'an unknown directory'}, not ${job.plan.cwd} — left for you to answer`, YELLOW);
|
|
6585
|
+
log(` ${getSessionDisplayName(job.plan.internalName) || job.plan.internalName}: claude is asking to trust ${trust.asked || 'an unknown directory'}, not ${job.plan.cwd} — left for you to answer`, YELLOW);
|
|
6575
6586
|
finish('trust-prompt');
|
|
6576
6587
|
}
|
|
6577
6588
|
continue;
|
|
@@ -15362,7 +15373,7 @@ async function attachToAllSessions(existingTunnel) {
|
|
|
15362
15373
|
const KNOWN_COMMANDS = [
|
|
15363
15374
|
'client', 'start', 'attach', 'local', 'stop', 'status', 'doctor', 'logs',
|
|
15364
15375
|
'help', 'config', 'url', 'qr', 'health', 'domain', 'domain-clear',
|
|
15365
|
-
'password', 'user', 'workspace', 'session', 'wipe', 'restore', 'update', 'resume-waiting', 'claude-hook', 'codex-notify', 'codex-hook', 'claude-skill', 'agent-skill', 'math', 'view', 'port', 'notify', 'whoami', 'new',
|
|
15376
|
+
'password', 'user', 'workspace', 'session', 'wipe', 'restore', 'recover', 'update', 'resume-waiting', 'claude-hook', 'codex-notify', 'codex-hook', 'claude-skill', 'agent-skill', 'math', 'view', 'port', 'notify', 'whoami', 'new',
|
|
15366
15377
|
'mail', 'registration', 'canonical'
|
|
15367
15378
|
];
|
|
15368
15379
|
|
|
@@ -15485,6 +15496,7 @@ const HELP_SECTIONS = [
|
|
|
15485
15496
|
] },
|
|
15486
15497
|
{ title: 'Restore', lines: [
|
|
15487
15498
|
`hop restore [--dry-run] [--agents] Recreate your saved sessions, resuming claude conversations (--agents includes agent-created ones)`,
|
|
15499
|
+
`hop recover [<session>] Bring back the agent that last ran here: inside a session, relaunch its conversation in this terminal`,
|
|
15488
15500
|
`hop restore <name>... Redo just those sessions — even live ones, if they hold only an idle shell or a startup dialog`,
|
|
15489
15501
|
`hop restore --health Show sessions the reconciler could not bring back on its own, and why`,
|
|
15490
15502
|
`hop update [--check|--no-host] Self-update from GitHub (daemon + host restart, host waits for the fleet to go idle; --no-host skips the host cycle)`,
|
|
@@ -16096,6 +16108,61 @@ async function main() {
|
|
|
16096
16108
|
// Handle `hop resume-waiting` — answer claude's resume question for any
|
|
16097
16109
|
// session currently parked on it (sessions restored before this existed,
|
|
16098
16110
|
// or restored with resume_mode=ask and since reconsidered).
|
|
16111
|
+
// hop recover [<session>] — bring back the agent that last ran in a
|
|
16112
|
+
// session. Inside a session with no argument, it relaunches that
|
|
16113
|
+
// session's own conversation IN THIS TERMINAL (the same flag-preserving
|
|
16114
|
+
// command `hop restore` would use), so a claude that exited — crash,
|
|
16115
|
+
// /exit, a wrong answer to a dialog — is one command away, and any
|
|
16116
|
+
// dialog it shows is answered by the human sitting there. Named from
|
|
16117
|
+
// anywhere, it is `hop restore <name>`.
|
|
16118
|
+
if (process.argv[2] === 'recover') {
|
|
16119
|
+
const args = process.argv.slice(3);
|
|
16120
|
+
const dryRun = args.includes('--dry-run') || args.includes('-n');
|
|
16121
|
+
const named = args.find((a) => !a.startsWith('-')) || null;
|
|
16122
|
+
const here = process.env.HOP_SESSION || null;
|
|
16123
|
+
if (!named && !here) {
|
|
16124
|
+
log(`Run ${CYAN}hop recover${RESET} inside a hop session, or name one: ${CYAN}hop recover <session>${RESET}`, YELLOW);
|
|
16125
|
+
process.exit(1);
|
|
16126
|
+
}
|
|
16127
|
+
const target = named ? (resolveSessionInternalName(named, named) || named) : here;
|
|
16128
|
+
if (target !== here) {
|
|
16129
|
+
// Not this terminal: the daemon relaunches it in its own room.
|
|
16130
|
+
const child = spawn(process.execPath, [__filename, 'restore', ...(dryRun ? ['--dry-run'] : []), named], { stdio: 'inherit' });
|
|
16131
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
16132
|
+
return;
|
|
16133
|
+
}
|
|
16134
|
+
const display = getSessionDisplayName(target) || target;
|
|
16135
|
+
const meta = readClaudeSessionMeta(target);
|
|
16136
|
+
const cfg = getSessionConfig(target);
|
|
16137
|
+
const cwd = meta?.cwd || cfg?.cwd || process.cwd();
|
|
16138
|
+
const rec = readClaudeSessionRecord(target);
|
|
16139
|
+
if (rec?.sessionId && claudeProcessRunning(rec.sessionId)) {
|
|
16140
|
+
log(`${display}: its claude conversation ${String(rec.sessionId).slice(0, 8)}… is already running in this session.`, YELLOW);
|
|
16141
|
+
process.exit(1);
|
|
16142
|
+
}
|
|
16143
|
+
const plan = restorePlanForSession({ internalName: target, cwd, startup: cfg?.startup, autoStart: cfg?.autoStart });
|
|
16144
|
+
let command = plan?.command || null;
|
|
16145
|
+
let kind = command ? 'claude' : null;
|
|
16146
|
+
if (!command) {
|
|
16147
|
+
// No claude record: a codex conversation is found by its rollout.
|
|
16148
|
+
const rollout = resolveCodexTranscriptByCwd(cwd);
|
|
16149
|
+
const rolloutId = rollout ? await trajectoryExtract.codexRolloutId(rollout) : null;
|
|
16150
|
+
if (rolloutId) {
|
|
16151
|
+
command = `codex resume '${rolloutId.replace(/'/g, `'\\''`)}'`;
|
|
16152
|
+
kind = 'codex';
|
|
16153
|
+
}
|
|
16154
|
+
}
|
|
16155
|
+
if (!command) {
|
|
16156
|
+
log(`${display}: nothing to recover — no claude or codex conversation is recorded for this session${plan?.warning ? ` (${plan.warning})` : ''}.`, YELLOW);
|
|
16157
|
+
process.exit(1);
|
|
16158
|
+
}
|
|
16159
|
+
log(`${dryRun ? 'Would recover' : 'Recovering'} ${BOLD}${display}${RESET} (${kind}) in this terminal:`, RESET);
|
|
16160
|
+
log(` ${DIM}${command}${RESET}`, RESET);
|
|
16161
|
+
if (dryRun) process.exit(0);
|
|
16162
|
+
const child = spawn('/bin/zsh', ['-lc', command], { stdio: 'inherit', cwd: plan?.cwd || cwd, env: process.env });
|
|
16163
|
+
child.on('exit', (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
16164
|
+
return;
|
|
16165
|
+
}
|
|
16099
16166
|
if (process.argv[2] === 'resume-waiting') {
|
|
16100
16167
|
const state = isServerRunning();
|
|
16101
16168
|
if (!state) { printNotRunningError(); process.exit(1); }
|
|
@@ -16258,7 +16325,7 @@ async function main() {
|
|
|
16258
16325
|
log(`${na.length} session(s) need your attention:`, YELLOW);
|
|
16259
16326
|
for (const it of na) {
|
|
16260
16327
|
log(` ${YELLOW}⚠${RESET} ${it.name} ${DIM}${it.reason}${RESET}`, RESET);
|
|
16261
|
-
log(` ${DIM}redo: ${CYAN}hop restore ${it.name}${RESET}`, RESET);
|
|
16328
|
+
log(` ${DIM}redo: ${CYAN}hop restore ${it.name}${RESET}${DIM}, or inside it: ${CYAN}hop recover${RESET}`, RESET);
|
|
16262
16329
|
}
|
|
16263
16330
|
process.exit(0);
|
|
16264
16331
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// The directory the dialog is asking about, so an answer is only ever given
|
|
5
|
+
// for the workspace the session was launched in - never for wherever a
|
|
6
|
+
// shell happened to wander.
|
|
7
|
+
function trustDialogWorkspace(screenText) {
|
|
8
|
+
const m = /Accessing\s*workspace:?\s*(\/[^\s\u2502\u2500]+)/i.exec(String(screenText || ''));
|
|
9
|
+
return m ? m[1].replace(/\/+$/, '') : null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What a trust dialog on screen is asking, and the exact keys that say yes.
|
|
13
|
+
*
|
|
14
|
+
* Two variants exist. The classic one names the workspace ("Accessing
|
|
15
|
+
* workspace: /path") and numbers its options ("1. Yes, proceed / 2. No,
|
|
16
|
+
* exit"): typing the number answers it. The other appears when the folder's
|
|
17
|
+
* .claude/settings.json pre-approves tool permissions: it names no path (the
|
|
18
|
+
* folder IS the launch directory), lists "No, exit" FIRST, and is an ARROW
|
|
19
|
+
* SELECTOR — a ❯ marks the highlighted option and Enter confirms it. Typing
|
|
20
|
+
* a digit does nothing there, and the Enter that follows confirms the
|
|
21
|
+
* default: "No, exit". That is exactly how motif's restore was declined
|
|
22
|
+
* three times over — "1\r" on 2026-09-04, "2\r" on 2026-09-10 — and the
|
|
23
|
+
* session came back as a shell each time. So the answer is read off the
|
|
24
|
+
* screen: the number when the options are numbered, otherwise enough arrow
|
|
25
|
+
* presses to move the highlight onto the yes option, then Enter.
|
|
26
|
+
*/
|
|
27
|
+
function trustDialogAnswer(screenText, sessionCwd) {
|
|
28
|
+
const text = String(screenText || '');
|
|
29
|
+
const asked = trustDialogWorkspace(text);
|
|
30
|
+
const preapproval = /pre-?approves\s*\d+\s*tool\s*permissions|Only\s*proceed\s*if\s*you\s*trust\s*this\s*configuration/i.test(text);
|
|
31
|
+
const lines = text.split('\n');
|
|
32
|
+
const isYes = (l) => /Yes,?\s*(I\s*trust\s*this\s*folder|proceed)/i.test(l);
|
|
33
|
+
const isOption = (l) => isYes(l) || /No,?\s*exit/i.test(l);
|
|
34
|
+
const yesIdx = lines.findIndex(isYes);
|
|
35
|
+
let input = '1\r';
|
|
36
|
+
let how = 'option 1';
|
|
37
|
+
if (yesIdx >= 0) {
|
|
38
|
+
const numbered = /(?:^|\s)(\d+)\.\s*Yes/i.exec(lines[yesIdx]);
|
|
39
|
+
if (numbered) {
|
|
40
|
+
input = `${numbered[1]}\r`;
|
|
41
|
+
how = `option ${numbered[1]}`;
|
|
42
|
+
} else {
|
|
43
|
+
// Highlighted row → yes row, counting only option rows between.
|
|
44
|
+
const highlighted = lines.findIndex((l) => /[❯›>]\s*(Yes|No)/i.test(l));
|
|
45
|
+
const from = highlighted >= 0 ? highlighted : lines.findIndex(isOption);
|
|
46
|
+
const between = lines.slice(Math.min(from, yesIdx), Math.max(from, yesIdx)).filter(isOption).length;
|
|
47
|
+
const steps = from === yesIdx ? 0 : Math.max(0, between - (from < yesIdx ? 0 : 0));
|
|
48
|
+
const arrow = from < yesIdx ? '\x1b[B' : '\x1b[A';
|
|
49
|
+
input = arrow.repeat(from === yesIdx ? 0 : Math.max(1, steps)) + '\r';
|
|
50
|
+
how = from === yesIdx ? 'Enter on the highlighted yes' : `${from < yesIdx ? 'down' : 'up'} ×${Math.max(1, steps)} + Enter`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const ours = asked ? samePath(asked, sessionCwd) : preapproval;
|
|
54
|
+
return { input, how, asked, preapproval, ours };
|
|
55
|
+
}
|
|
56
|
+
function samePath(a, b) {
|
|
57
|
+
const norm = (p) => {
|
|
58
|
+
let v = String(p || '').replace(/\/+$/, '');
|
|
59
|
+
try { v = fs.realpathSync(v); } catch (e) { /* keep as-is */ }
|
|
60
|
+
return v;
|
|
61
|
+
};
|
|
62
|
+
return !!a && !!b && norm(a) === norm(b);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { trustDialogWorkspace, trustDialogAnswer, samePath };
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { trustDialogAnswer } = require('../lib/trust-dialog');
|
|
4
|
+
|
|
5
|
+
const classic = `
|
|
6
|
+
Quick safety check
|
|
7
|
+
Accessing workspace: /Users/me/proj
|
|
8
|
+
Is this a project you created or one you trust?
|
|
9
|
+
❯ 1. Yes, proceed
|
|
10
|
+
2. No, exit
|
|
11
|
+
`;
|
|
12
|
+
const preapproval = `
|
|
13
|
+
⚠ This folder pre-approves 15 tool permissions in .claude/settings.json:
|
|
14
|
+
mcp__x__read, mcp__y__write, and 7 more
|
|
15
|
+
These will apply without asking. Only proceed if you trust this configuration.
|
|
16
|
+
❯ No, exit
|
|
17
|
+
Yes, I trust this folder
|
|
18
|
+
Enter to confirm · Esc to cancel
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
test('classic numbered dialog: type the number of the yes option, only for the session\'s own workspace', () => {
|
|
22
|
+
const ours = trustDialogAnswer(classic, '/Users/me/proj');
|
|
23
|
+
assert.equal(ours.input, '1\r');
|
|
24
|
+
assert.equal(ours.asked, '/Users/me/proj');
|
|
25
|
+
assert.equal(ours.ours, true);
|
|
26
|
+
assert.equal(trustDialogAnswer(classic, '/Users/me/elsewhere').ours, false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('pre-approval dialog is an arrow selector with "No, exit" highlighted: Down then Enter (the motif regression)', () => {
|
|
30
|
+
const a = trustDialogAnswer(preapproval, '/Users/me');
|
|
31
|
+
assert.equal(a.preapproval, true);
|
|
32
|
+
assert.equal(a.input, '\x1b[B\r');
|
|
33
|
+
assert.equal(a.asked, null); // names no path: the folder is the launch directory
|
|
34
|
+
assert.equal(a.ours, true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('a selector already highlighting yes needs only Enter', () => {
|
|
38
|
+
const t = preapproval.replace('❯ No, exit', ' No, exit').replace(' Yes, I trust', '❯ Yes, I trust');
|
|
39
|
+
assert.equal(trustDialogAnswer(t, '/Users/me').input, '\r');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a numbered pre-approval layout takes its number', () => {
|
|
43
|
+
const t = preapproval.replace('❯ No, exit', ' 1. No, exit').replace(' Yes, I trust', '❯ 2. Yes, I trust');
|
|
44
|
+
assert.equal(trustDialogAnswer(t, '/Users/me').input, '2\r');
|
|
45
|
+
});
|