@phnx-labs/agents-cli 1.22.22 → 1.22.24
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/CHANGELOG.md +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
package/dist/commands/fork.js
CHANGED
|
@@ -3,71 +3,98 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
3
3
|
import { findSessionsById } from '../lib/session/db.js';
|
|
4
4
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
5
5
|
import { forkSession, isForkableAgent, FORKABLE_AGENTS } from '../lib/session/fork.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const FORK_HELP = {
|
|
7
|
+
examples: `
|
|
8
|
+
# Fork a session by (partial) id, then continue the fork
|
|
9
|
+
agents sessions fork 4f3a9c21
|
|
10
|
+
agents sessions resume <new-id>
|
|
11
|
+
|
|
12
|
+
# Give the fork a name
|
|
13
|
+
agents sessions fork 4f3a9c21 --name "try redis instead"
|
|
14
|
+
`,
|
|
15
|
+
notes: `
|
|
16
|
+
- 'resume' continues the SAME conversation; 'fork' copies it under a new id so the two diverge.
|
|
17
|
+
- The fork is a full copy of the conversation so far; continuing it never touches the original.
|
|
18
|
+
- Resolve the session the same way as resume: an exact or prefix id fragment.
|
|
19
|
+
- Native copy currently supports: ${FORKABLE_AGENTS.join(', ')}. For other harnesses, branch by
|
|
20
|
+
starting a fresh agent and seeding it with '/continue <id>' — the source stays put.
|
|
21
|
+
`,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the source session, copy it under a fresh id, and print how to
|
|
25
|
+
* continue the fork. Shared by `agents sessions fork` and the `agents fork` alias.
|
|
26
|
+
*/
|
|
27
|
+
export async function runFork(sessionArg, options) {
|
|
28
|
+
// Resolve the source. Try the index first; only pay for a rescan if the id
|
|
29
|
+
// isn't found yet (mirrors the resume path's freshen-then-lookup).
|
|
30
|
+
let matches = findSessionsById(sessionArg, {});
|
|
31
|
+
if (matches.length === 0) {
|
|
32
|
+
await discoverSessions({});
|
|
33
|
+
matches = findSessionsById(sessionArg, {});
|
|
34
|
+
}
|
|
35
|
+
if (matches.length === 0) {
|
|
36
|
+
// Errors go to stderr and set a non-zero exit code so a script chaining on
|
|
37
|
+
// `agents sessions fork <id> && agents sessions resume <new>` doesn't proceed
|
|
38
|
+
// on a failed fork.
|
|
39
|
+
console.error(chalk.red(`No session matching "${sessionArg}".`));
|
|
40
|
+
console.error(chalk.gray('List candidates with: agents sessions'));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (matches.length > 1) {
|
|
45
|
+
console.error(chalk.yellow(`"${sessionArg}" is ambiguous — ${matches.length} sessions match. Use a longer id:`));
|
|
46
|
+
for (const m of matches.slice(0, 8)) {
|
|
47
|
+
console.error(chalk.gray(` ${m.shortId} ${m.agent} ${m.label || m.topic || ''}`));
|
|
48
|
+
}
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const source = matches[0];
|
|
53
|
+
if (!isForkableAgent(source.agent)) {
|
|
54
|
+
// A native copy needs the agent's transcript to be resumable by id; harnesses
|
|
55
|
+
// without that can still be branched by hand. Fail loud with the manual path
|
|
56
|
+
// rather than a silent no-op or a fake copy.
|
|
57
|
+
console.error(chalk.yellow(`A native fork copy isn't supported for ${source.agent} sessions yet (supported: ${FORKABLE_AGENTS.join(', ')}).`));
|
|
58
|
+
console.error(chalk.gray(` Branch it by hand — start a fresh ${source.agent} and seed it with the source's context:`));
|
|
59
|
+
console.error(chalk.gray(` agents run ${source.agent} --terminal # then, in the new session:`));
|
|
60
|
+
console.error(chalk.gray(` /continue ${source.shortId}`));
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let result;
|
|
65
|
+
try {
|
|
66
|
+
result = forkSession(source, { name: options.name });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error(chalk.red(`Could not fork ${source.shortId}: ${err.message}`));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(chalk.green(`Forked ${source.shortId} -> ${result.shortId}`));
|
|
74
|
+
console.log(chalk.gray(` Label: ${result.label}`));
|
|
75
|
+
console.log(chalk.gray(` Continue: agents sessions resume ${result.shortId}`));
|
|
76
|
+
console.log(chalk.gray(` Original ${source.shortId} is untouched.`));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Register `agents sessions fork <session>` — the canonical surface (fork is a
|
|
80
|
+
* session operation, so it lives under the `sessions` group).
|
|
81
|
+
*/
|
|
82
|
+
export function registerSessionsForkCommand(sessionsCmd) {
|
|
83
|
+
const cmd = sessionsCmd
|
|
9
84
|
.command('fork <session>')
|
|
10
85
|
.description('Branch a session into a new, independent copy you can continue separately. The original is untouched.')
|
|
11
86
|
.option('--name <label>', 'Label for the fork (default: "fork of <original>")');
|
|
12
|
-
setHelpSections(cmd,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- Currently supports: ${FORKABLE_AGENTS.join(', ')}. Other agents are a planned follow-up.
|
|
26
|
-
`,
|
|
27
|
-
});
|
|
28
|
-
cmd.action(async (sessionArg, options) => {
|
|
29
|
-
// Resolve the source. Try the index first; only pay for a rescan if the id
|
|
30
|
-
// isn't found yet (mirrors the resume path's freshen-then-lookup).
|
|
31
|
-
let matches = findSessionsById(sessionArg, {});
|
|
32
|
-
if (matches.length === 0) {
|
|
33
|
-
await discoverSessions({});
|
|
34
|
-
matches = findSessionsById(sessionArg, {});
|
|
35
|
-
}
|
|
36
|
-
if (matches.length === 0) {
|
|
37
|
-
// Errors go to stderr and set a non-zero exit code so a script chaining on
|
|
38
|
-
// `agents fork <id> && agents resume <new>` doesn't proceed on a failed fork.
|
|
39
|
-
console.error(chalk.red(`No session matching "${sessionArg}".`));
|
|
40
|
-
console.error(chalk.gray('List candidates with: agents sessions'));
|
|
41
|
-
process.exitCode = 1;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (matches.length > 1) {
|
|
45
|
-
console.error(chalk.yellow(`"${sessionArg}" is ambiguous — ${matches.length} sessions match. Use a longer id:`));
|
|
46
|
-
for (const m of matches.slice(0, 8)) {
|
|
47
|
-
console.error(chalk.gray(` ${m.shortId} ${m.agent} ${m.label || m.topic || ''}`));
|
|
48
|
-
}
|
|
49
|
-
process.exitCode = 1;
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const source = matches[0];
|
|
53
|
-
if (!isForkableAgent(source.agent)) {
|
|
54
|
-
console.error(chalk.yellow(`fork does not support ${source.agent} sessions yet.`));
|
|
55
|
-
console.error(chalk.gray(` Supported: ${FORKABLE_AGENTS.join(', ')}.`));
|
|
56
|
-
process.exitCode = 1;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
let result;
|
|
60
|
-
try {
|
|
61
|
-
result = forkSession(source, { name: options.name });
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
console.error(chalk.red(`Could not fork ${source.shortId}: ${err.message}`));
|
|
65
|
-
process.exitCode = 1;
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
console.log(chalk.green(`Forked ${source.shortId} -> ${result.shortId}`));
|
|
69
|
-
console.log(chalk.gray(` Label: ${result.label}`));
|
|
70
|
-
console.log(chalk.gray(` Continue: agents resume ${result.shortId}`));
|
|
71
|
-
console.log(chalk.gray(` Original ${source.shortId} is untouched.`));
|
|
72
|
-
});
|
|
87
|
+
setHelpSections(cmd, FORK_HELP);
|
|
88
|
+
cmd.action(runFork);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Register the hidden top-level `agents fork` alias. Kept working for back-compat
|
|
92
|
+
* and muscle memory; the canonical, discoverable surface is `agents sessions fork`.
|
|
93
|
+
*/
|
|
94
|
+
export function registerForkCommand(program) {
|
|
95
|
+
const cmd = program
|
|
96
|
+
.command('fork <session>', { hidden: true })
|
|
97
|
+
.description('Alias for `agents sessions fork` — branch a session into a new, independent copy.')
|
|
98
|
+
.option('--name <label>', 'Label for the fork (default: "fork of <original>")');
|
|
99
|
+
cmd.action(runFork);
|
|
73
100
|
}
|
package/dist/commands/go.d.ts
CHANGED
|
@@ -16,21 +16,46 @@
|
|
|
16
16
|
import type { Command } from 'commander';
|
|
17
17
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
18
18
|
import type { SessionMeta } from '../lib/session/types.js';
|
|
19
|
+
import { type LiveStatusFilter } from './sessions.js';
|
|
19
20
|
export declare function registerGoCommand(program: Command): void;
|
|
21
|
+
/**
|
|
22
|
+
* Scope a live-session pool by device and live status. Pure so the `focus`
|
|
23
|
+
* device/status filters are unit-testable without touching the sweep. `hosts`
|
|
24
|
+
* keeps only sessions whose `machine` is in the set (local rows carry `self`,
|
|
25
|
+
* remote rows carry their peer tag); `statuses` reuses `--active`'s exact
|
|
26
|
+
* `matchesLiveStatus` derivation rather than a parallel status table.
|
|
27
|
+
*/
|
|
28
|
+
export declare function filterLivePool(sessions: ActiveSession[], opts?: {
|
|
29
|
+
hosts?: string[];
|
|
30
|
+
statuses?: LiveStatusFilter[];
|
|
31
|
+
}): ActiveSession[];
|
|
20
32
|
/**
|
|
21
33
|
* Live jump targets (local + remote), keyed by session id. Cloud is excluded by
|
|
22
34
|
* default (it has no local pid to attach), but `detach` opts in with
|
|
23
35
|
* `includeCloud` so it can resolve a cloud id and refuse it with a clear message
|
|
24
36
|
* instead of a bare "no live session".
|
|
37
|
+
*
|
|
38
|
+
* `hosts` scopes the sweep to named devices — the fan-out only dials them, and the
|
|
39
|
+
* pool is then filtered to `s.machine ∈ hosts` so a stray local row can't leak in.
|
|
40
|
+
* `statuses` narrows to the live-state words `--active` uses (orphan/crashed/…).
|
|
25
41
|
*/
|
|
26
42
|
export declare function gatherLiveTargets(local: boolean, opts?: {
|
|
27
43
|
includeCloud?: boolean;
|
|
44
|
+
hosts?: string[];
|
|
45
|
+
statuses?: LiveStatusFilter[];
|
|
28
46
|
}): Promise<{
|
|
29
47
|
self: string;
|
|
30
48
|
activeById: Map<string, ActiveSession>;
|
|
31
49
|
}>;
|
|
32
50
|
/** Interactive pick over the live sessions' rich SessionMeta; returns the chosen live session. */
|
|
33
51
|
export declare function pickLiveTarget(activeById: Map<string, ActiveSession>, self: string, message: string, enterHint: string): Promise<ActiveSession | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Multi-select over the live sessions' rich SessionMeta (same rows as
|
|
54
|
+
* `pickLiveTarget`, but a checkbox picker) — the plural sibling that lets `focus`
|
|
55
|
+
* open several sessions at once. Mirrors `sessions resume`'s `multiItemPicker`
|
|
56
|
+
* wiring; returns the chosen live sessions in pick order, or `[]` on cancel.
|
|
57
|
+
*/
|
|
58
|
+
export declare function pickLiveTargets(activeById: Map<string, ActiveSession>, self: string, message: string): Promise<ActiveSession[]>;
|
|
34
59
|
/**
|
|
35
60
|
* Map each live session to its rich SessionMeta (worktree/PR/changes/tools/tests
|
|
36
61
|
* via the shared picker), reusing `discoverSessions`. Remote or unindexed live
|
|
@@ -53,6 +78,19 @@ export declare function describeWhere(s: ActiveSession, self: string): Where;
|
|
|
53
78
|
* the session lives on another machine, else undefined.
|
|
54
79
|
*/
|
|
55
80
|
export type UnreachableFallback = (s: ActiveSession, remote: string | undefined) => void | Promise<void>;
|
|
56
|
-
|
|
81
|
+
export type AttachRailLiveness = {
|
|
82
|
+
state: 'alive';
|
|
83
|
+
} | {
|
|
84
|
+
state: 'dead';
|
|
85
|
+
exitStatus?: number;
|
|
86
|
+
} | {
|
|
87
|
+
state: 'missing';
|
|
88
|
+
};
|
|
89
|
+
/** Probe the tmux process, not just retained provenance. This is deliberately
|
|
90
|
+
* called immediately before an attach so remain-on-exit panes cannot masquerade
|
|
91
|
+
* as living agent sessions. */
|
|
92
|
+
export declare function probeAttachRail(s: ActiveSession, self: string): Promise<AttachRailLiveness>;
|
|
93
|
+
/** Strict attach-only fallback: no pane means no attach. Never open a shell or
|
|
94
|
+
* start recovery, because both would violate the caller's no-fork intent. */
|
|
57
95
|
export declare function refuseFallback(s: ActiveSession, remote: string | undefined): Promise<void>;
|
|
58
96
|
export declare function jumpTo(s: ActiveSession, self: string, fallback?: UnreachableFallback): Promise<void>;
|
package/dist/commands/go.js
CHANGED
|
@@ -21,12 +21,16 @@ import { getActiveSessions, findSessionFileForKind } from '../lib/session/active
|
|
|
21
21
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
22
22
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
23
23
|
import { deriveShortId } from '../lib/session/short-id.js';
|
|
24
|
-
import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive } from './sessions.js';
|
|
24
|
+
import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive, matchesLiveStatus, filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, } from './sessions.js';
|
|
25
|
+
import { buildPreview } from './sessions-picker.js';
|
|
26
|
+
import { multiItemPicker } from '../lib/picker.js';
|
|
27
|
+
import { isPromptCancelled } from './utils.js';
|
|
25
28
|
import { focusAction } from './focus.js';
|
|
26
29
|
import { machineId } from '../lib/session/sync/config.js';
|
|
27
30
|
import { attachTmux, runTmux } from '../lib/tmux/binary.js';
|
|
31
|
+
import { paneExitStatus } from '../lib/tmux/session.js';
|
|
28
32
|
import { getDefaultSocketPath } from '../lib/tmux/paths.js';
|
|
29
|
-
import { sshStream, assertValidSshTarget, shellQuote } from '../lib/ssh-exec.js';
|
|
33
|
+
import { sshExec, sshStream, assertValidSshTarget, shellQuote } from '../lib/ssh-exec.js';
|
|
30
34
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
31
35
|
const execFileAsync = promisify(execFile);
|
|
32
36
|
export function registerGoCommand(program) {
|
|
@@ -40,11 +44,33 @@ export function registerGoCommand(program) {
|
|
|
40
44
|
await focusAction(id, { local: opts.local, attachOnly: true });
|
|
41
45
|
});
|
|
42
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Scope a live-session pool by device and live status. Pure so the `focus`
|
|
49
|
+
* device/status filters are unit-testable without touching the sweep. `hosts`
|
|
50
|
+
* keeps only sessions whose `machine` is in the set (local rows carry `self`,
|
|
51
|
+
* remote rows carry their peer tag); `statuses` reuses `--active`'s exact
|
|
52
|
+
* `matchesLiveStatus` derivation rather than a parallel status table.
|
|
53
|
+
*/
|
|
54
|
+
export function filterLivePool(sessions, opts = {}) {
|
|
55
|
+
let out = sessions;
|
|
56
|
+
if (opts.hosts?.length) {
|
|
57
|
+
const set = new Set(opts.hosts);
|
|
58
|
+
out = out.filter((s) => !!s.machine && set.has(s.machine));
|
|
59
|
+
}
|
|
60
|
+
if (opts.statuses?.length) {
|
|
61
|
+
out = out.filter((s) => opts.statuses.some((status) => matchesLiveStatus(s, status)));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
43
65
|
/**
|
|
44
66
|
* Live jump targets (local + remote), keyed by session id. Cloud is excluded by
|
|
45
67
|
* default (it has no local pid to attach), but `detach` opts in with
|
|
46
68
|
* `includeCloud` so it can resolve a cloud id and refuse it with a clear message
|
|
47
69
|
* instead of a bare "no live session".
|
|
70
|
+
*
|
|
71
|
+
* `hosts` scopes the sweep to named devices — the fan-out only dials them, and the
|
|
72
|
+
* pool is then filtered to `s.machine ∈ hosts` so a stray local row can't leak in.
|
|
73
|
+
* `statuses` narrows to the live-state words `--active` uses (orphan/crashed/…).
|
|
48
74
|
*/
|
|
49
75
|
export async function gatherLiveTargets(local, opts = {}) {
|
|
50
76
|
const self = machineId();
|
|
@@ -55,11 +81,12 @@ export async function gatherLiveTargets(local, opts = {}) {
|
|
|
55
81
|
let active = localActive;
|
|
56
82
|
if (!local) {
|
|
57
83
|
try {
|
|
58
|
-
const remote = await gatherRemoteActive();
|
|
84
|
+
const remote = await gatherRemoteActive(opts.hosts);
|
|
59
85
|
active = dedupeByMachineSession([...localActive, ...remote.sessions]);
|
|
60
86
|
}
|
|
61
87
|
catch { /* remote sweep is best-effort */ }
|
|
62
88
|
}
|
|
89
|
+
active = filterLivePool(active, { hosts: opts.hosts, statuses: opts.statuses });
|
|
63
90
|
const activeById = new Map();
|
|
64
91
|
for (const s of active) {
|
|
65
92
|
if (!s.sessionId)
|
|
@@ -80,6 +107,41 @@ export async function pickLiveTarget(activeById, self, message, enterHint) {
|
|
|
80
107
|
return null;
|
|
81
108
|
return activeById.get(picked.session.id) ?? null;
|
|
82
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Multi-select over the live sessions' rich SessionMeta (same rows as
|
|
112
|
+
* `pickLiveTarget`, but a checkbox picker) — the plural sibling that lets `focus`
|
|
113
|
+
* open several sessions at once. Mirrors `sessions resume`'s `multiItemPicker`
|
|
114
|
+
* wiring; returns the chosen live sessions in pick order, or `[]` on cancel.
|
|
115
|
+
*/
|
|
116
|
+
export async function pickLiveTargets(activeById, self, message) {
|
|
117
|
+
const pool = await buildLivePool(activeById, self);
|
|
118
|
+
if (pool.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
// gutter: 6 = the multi-select cursor + checkbox ('> [x] ') multiItemPicker prepends.
|
|
121
|
+
const cols = { ...pickerColumnsFor(pool), gutter: 6 };
|
|
122
|
+
let chosen;
|
|
123
|
+
try {
|
|
124
|
+
chosen = await multiItemPicker({
|
|
125
|
+
message,
|
|
126
|
+
items: pool,
|
|
127
|
+
filter: (q) => (q.trim() ? filterSessionsByQuery(pool, q) : pool),
|
|
128
|
+
labelFor: (s, q) => formatPickerLabel(s, q, cols),
|
|
129
|
+
keyFor: (s) => s.id,
|
|
130
|
+
buildPreview,
|
|
131
|
+
pageSize: 15,
|
|
132
|
+
emptyMessage: 'No live sessions match.',
|
|
133
|
+
enterHint: 'focus',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (isPromptCancelled(err))
|
|
138
|
+
return [];
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
if (!chosen)
|
|
142
|
+
return [];
|
|
143
|
+
return chosen.map((m) => activeById.get(m.id)).filter((s) => !!s);
|
|
144
|
+
}
|
|
83
145
|
/**
|
|
84
146
|
* Map each live session to its rich SessionMeta (worktree/PR/changes/tools/tests
|
|
85
147
|
* via the shared picker), reusing `discoverSessions`. Remote or unindexed live
|
|
@@ -147,15 +209,48 @@ export function describeWhere(s, self) {
|
|
|
147
209
|
return { label: `${s.host ?? 'shell'} on ${remote}`, action: `open a shell on ${remote}` };
|
|
148
210
|
return { label: s.host ?? 'unknown terminal', action: 'resume it (no live attach rail)' };
|
|
149
211
|
}
|
|
150
|
-
/**
|
|
212
|
+
/** Probe the tmux process, not just retained provenance. This is deliberately
|
|
213
|
+
* called immediately before an attach so remain-on-exit panes cannot masquerade
|
|
214
|
+
* as living agent sessions. */
|
|
215
|
+
export async function probeAttachRail(s, self) {
|
|
216
|
+
const mux = s.provenance?.mux;
|
|
217
|
+
if (mux?.kind !== 'tmux' || !mux.pane)
|
|
218
|
+
return { state: 'missing' };
|
|
219
|
+
const remote = s.machine && s.machine !== self ? s.machine : undefined;
|
|
220
|
+
if (!remote) {
|
|
221
|
+
const pane = await paneExitStatus(mux.pane, mux.socket ?? getDefaultSocketPath());
|
|
222
|
+
if (!pane.found)
|
|
223
|
+
return { state: 'missing' };
|
|
224
|
+
return pane.dead ? { state: 'dead', exitStatus: pane.status } : { state: 'alive' };
|
|
225
|
+
}
|
|
226
|
+
assertValidSshTarget(remote);
|
|
227
|
+
const sock = mux.socket ? `-S ${shellQuote(mux.socket)} ` : '';
|
|
228
|
+
const pane = shellQuote(mux.pane);
|
|
229
|
+
const command = `v=$(tmux ${sock}display-message -pt ${pane} -p '#{pane_dead} #{pane_dead_status}' 2>/dev/null) || { echo missing; exit 0; }; ` +
|
|
230
|
+
`printf '%s\\n' "$v"`;
|
|
231
|
+
const result = sshExec(remote, command, { timeoutMs: 15_000, multiplex: true });
|
|
232
|
+
if (result.code !== 0)
|
|
233
|
+
return { state: 'missing' };
|
|
234
|
+
const value = result.stdout.trim();
|
|
235
|
+
if (value === 'missing' || !value)
|
|
236
|
+
return { state: 'missing' };
|
|
237
|
+
const [dead, rawStatus] = value.split(/\s+/);
|
|
238
|
+
const exitStatus = Number.parseInt(rawStatus ?? '', 10);
|
|
239
|
+
return dead === '1'
|
|
240
|
+
? { state: 'dead', exitStatus: Number.isFinite(exitStatus) ? exitStatus : undefined }
|
|
241
|
+
: { state: 'alive' };
|
|
242
|
+
}
|
|
243
|
+
/** Strict attach-only fallback: no pane means no attach. Never open a shell or
|
|
244
|
+
* start recovery, because both would violate the caller's no-fork intent. */
|
|
151
245
|
export async function refuseFallback(s, remote) {
|
|
152
246
|
if (remote) {
|
|
153
|
-
console.log(chalk.yellow(
|
|
154
|
-
|
|
155
|
-
|
|
247
|
+
console.log(chalk.yellow(`Can't attach ${shortId(s)} on ${remote} — it has no living tmux pane.`));
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
156
250
|
}
|
|
157
251
|
console.log(chalk.yellow(`Can't jump to ${shortId(s)} — it's in ${s.host ?? 'an unknown terminal'} with no attach rail (not tmux/Ghostty).`) +
|
|
158
252
|
chalk.gray(`\nTry: agents sessions resume ${shortId(s)}`));
|
|
253
|
+
process.exitCode = 1;
|
|
159
254
|
}
|
|
160
255
|
export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
161
256
|
const remote = s.machine && s.machine !== self ? s.machine : undefined;
|
|
@@ -163,6 +258,11 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
163
258
|
// Path C: remote tmux — ssh in and attach, resolving the pane's session on the remote.
|
|
164
259
|
if (remote) {
|
|
165
260
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
261
|
+
const liveness = await probeAttachRail(s, self);
|
|
262
|
+
if (liveness.state !== 'alive') {
|
|
263
|
+
await fallback(s, remote);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
166
266
|
assertValidSshTarget(remote);
|
|
167
267
|
const sock = mux.socket ? `-S ${shellQuote(mux.socket)} ` : '';
|
|
168
268
|
const p = shellQuote(mux.pane);
|
|
@@ -179,6 +279,11 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
179
279
|
}
|
|
180
280
|
// Path B: local tmux — attach (or switch-client if we're already inside tmux).
|
|
181
281
|
if (mux?.kind === 'tmux' && mux.pane) {
|
|
282
|
+
const liveness = await probeAttachRail(s, self);
|
|
283
|
+
if (liveness.state !== 'alive') {
|
|
284
|
+
await fallback(s, undefined);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
182
287
|
const socket = mux.socket ?? getDefaultSocketPath();
|
|
183
288
|
const { session, window } = await resolveLocalPane(socket, mux.pane);
|
|
184
289
|
if (session && window != null) {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared interactive step engine for `agents harness` create + edit.
|
|
3
|
+
*
|
|
4
|
+
* A single engine drives BOTH the create wizard (`agents harness add`/`fork`,
|
|
5
|
+
* previously `runHarnessWizard`) and the new edit wizard (`agents harness edit`,
|
|
6
|
+
* which was flag-only before). A "step" is a self-contained unit — decide whether
|
|
7
|
+
* it runs for the current draft, then prompt/validate/apply — so the two modes
|
|
8
|
+
* differ only in their step list, not in the runner.
|
|
9
|
+
*
|
|
10
|
+
* Design goals (RUSH-2219, parent RUSH-2218):
|
|
11
|
+
* - One runner, two modes. `create` builds a new harness from a source; `edit`
|
|
12
|
+
* loads an existing profile and re-asks each field pre-filled with its value.
|
|
13
|
+
* - Every step is skippable. A flag that already supplied the value pre-fills
|
|
14
|
+
* the draft and its step is not re-asked — so non-interactive scripting via
|
|
15
|
+
* flags is unchanged and the wizard only asks for what is missing.
|
|
16
|
+
* - Pure and injectable. The engine talks to the user through {@link WizardIO},
|
|
17
|
+
* an injected seam. Tests drive a scripted fake IO and assert which steps ran,
|
|
18
|
+
* with no TTY. {@link defaultWizardIO} is the production driver over
|
|
19
|
+
* `@inquirer/prompts`.
|
|
20
|
+
* - Typed extension points, not stubs. The three sibling subtasks plug in via
|
|
21
|
+
* {@link WizardHooks} without editing the engine: RUSH-2220 (model catalog +
|
|
22
|
+
* secrets surface) via `pickModel`, RUSH-2221 (connection test) via
|
|
23
|
+
* `connectionTest`, RUSH-2222 (edit matrix) via `editable`. Each hook is a
|
|
24
|
+
* real no-op extension point — absent, the scaffold falls back to today's
|
|
25
|
+
* behavior (free-text model, no test, resolver-sourced editability). None of
|
|
26
|
+
* them fakes a result. RUSH-2223 (cross-host portability, `fork --to-host`)
|
|
27
|
+
* extends the create source step; the seam is `HarnessDraft.toHost`.
|
|
28
|
+
*/
|
|
29
|
+
import type { AgentId } from '../lib/types.js';
|
|
30
|
+
import { type Profile } from '../lib/profiles.js';
|
|
31
|
+
/** Whether the wizard is creating a new harness or editing an existing one. */
|
|
32
|
+
export type WizardMode = 'create' | 'edit';
|
|
33
|
+
/** A single `select` choice. `disabled` greys the row (used by the edit matrix). */
|
|
34
|
+
export interface WizardChoice<T> {
|
|
35
|
+
name: string;
|
|
36
|
+
value: T;
|
|
37
|
+
disabled?: boolean | string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The prompt seam the engine drives. Injected so the engine is testable with no
|
|
41
|
+
* TTY: the production implementation ({@link defaultWizardIO}) wraps
|
|
42
|
+
* `@inquirer/prompts`; a test passes a scripted fake that records calls and
|
|
43
|
+
* returns canned answers.
|
|
44
|
+
*/
|
|
45
|
+
export interface WizardIO {
|
|
46
|
+
select<T>(opts: {
|
|
47
|
+
message: string;
|
|
48
|
+
choices: WizardChoice<T>[];
|
|
49
|
+
default?: T;
|
|
50
|
+
}): Promise<T>;
|
|
51
|
+
input(opts: {
|
|
52
|
+
message: string;
|
|
53
|
+
default?: string;
|
|
54
|
+
validate?: (v: string) => true | string;
|
|
55
|
+
}): Promise<string>;
|
|
56
|
+
password(opts: {
|
|
57
|
+
message: string;
|
|
58
|
+
}): Promise<string>;
|
|
59
|
+
confirm(opts: {
|
|
60
|
+
message: string;
|
|
61
|
+
default?: boolean;
|
|
62
|
+
}): Promise<boolean>;
|
|
63
|
+
/** Emit an informational line — a disabled field's reason, or a preset note. */
|
|
64
|
+
note(message: string): void;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The mutable draft threaded through every step. It is a superset of both modes'
|
|
68
|
+
* fields; a step reads what it needs and records its answer here. `create` maps
|
|
69
|
+
* the finished draft into `{ source, name, opts }` for `runForkFlow`; `edit` maps
|
|
70
|
+
* it into the `EditOptions` shape the flag-driven edit path already persists — so
|
|
71
|
+
* a wizard-built and a hand-written harness are byte-identical after save.
|
|
72
|
+
*/
|
|
73
|
+
export interface HarnessDraft {
|
|
74
|
+
readonly mode: WizardMode;
|
|
75
|
+
/** create: the fork source (a native agent id or an existing harness name). */
|
|
76
|
+
source?: string;
|
|
77
|
+
/** The resolved host CLI — drives the editability seams. edit: the profile host. */
|
|
78
|
+
host?: AgentId;
|
|
79
|
+
name?: string;
|
|
80
|
+
model?: string;
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
authProvider?: string;
|
|
83
|
+
/** `<bundle>` or `<bundle>:<key>` — a value copied out of an agents secrets bundle. */
|
|
84
|
+
fromSecrets?: string;
|
|
85
|
+
version?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
fallbackModel?: string;
|
|
88
|
+
/** edit: the profile being edited, providing current values. Read-only. */
|
|
89
|
+
readonly original?: Profile;
|
|
90
|
+
/**
|
|
91
|
+
* RUSH-2223 seam (cross-host portability): the target host to re-key a cloned
|
|
92
|
+
* harness onto (`fork --to-host <host>`). The scaffold never sets it; the
|
|
93
|
+
* portability subtask reads it in an extended source step. Left here so the
|
|
94
|
+
* draft shape is stable when that lands.
|
|
95
|
+
*/
|
|
96
|
+
toHost?: AgentId;
|
|
97
|
+
/** The user chose "build custom" over a preset (create only). */
|
|
98
|
+
custom?: boolean;
|
|
99
|
+
/** The preset a create run selected, if any. */
|
|
100
|
+
preset?: string;
|
|
101
|
+
/** Pre-computed default for the name prompt (source or preset name). */
|
|
102
|
+
defaultName?: string;
|
|
103
|
+
/** The provider step has run (distinguishes "no auth chosen" from "not asked"). */
|
|
104
|
+
providerAsked?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What the engine does with a step for a given draft:
|
|
108
|
+
* - `'run'` prompt the user, validate, and apply into the draft.
|
|
109
|
+
* - `'skip'` value already supplied (by a flag) or step N/A — silent.
|
|
110
|
+
* - `{ disabled: … }` the step does not apply to this host; surface the reason
|
|
111
|
+
* (the field reads as greyed/disabled) but do not prompt.
|
|
112
|
+
* This is the seam the edit matrix (RUSH-2222) drives.
|
|
113
|
+
*/
|
|
114
|
+
export type StepDecision = 'run' | 'skip' | {
|
|
115
|
+
disabled: string;
|
|
116
|
+
};
|
|
117
|
+
/** One wizard step: decide, then (only when decided `'run'`) prompt + apply. */
|
|
118
|
+
export interface WizardStep {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
decide(draft: HarnessDraft): StepDecision;
|
|
121
|
+
run(io: WizardIO, draft: HarnessDraft, hooks: WizardHooks): Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
/** Per-host editability — which params this host's API format lets you change. */
|
|
124
|
+
export interface HarnessEditable {
|
|
125
|
+
model: boolean;
|
|
126
|
+
baseUrl: boolean;
|
|
127
|
+
auth: boolean;
|
|
128
|
+
version: boolean;
|
|
129
|
+
fallback: boolean;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolver-sourced editability, the scaffold default behind the RUSH-2222 seam.
|
|
133
|
+
* Every field is read from the same maps the run-time resolver uses
|
|
134
|
+
* (`baseUrlEnvKeyForHost` / `authEnvKeyForHost` / `isSelfUpdatingAgent`), never a
|
|
135
|
+
* table hardcoded alongside them — so the wizard's enable/disable can never drift
|
|
136
|
+
* from what a run actually honors (repo rule: the capability table stays truthful,
|
|
137
|
+
* in lockstep with the code). RUSH-2222 may replace this via {@link WizardHooks.editable}
|
|
138
|
+
* to add per-param reasons; it must stay sourced from the resolver.
|
|
139
|
+
*/
|
|
140
|
+
export declare function defaultEditable(host: AgentId): HarnessEditable;
|
|
141
|
+
/** Outcome of a connection test (RUSH-2221 fills the real classifier). */
|
|
142
|
+
export interface ConnectionTestResult {
|
|
143
|
+
ok: boolean;
|
|
144
|
+
/** Machine-readable class when it failed (auth / endpoint / model / unknown). */
|
|
145
|
+
reason?: 'auth' | 'endpoint' | 'model' | 'unknown';
|
|
146
|
+
message?: string;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Extension points the sibling subtasks fill without touching the engine. Each is
|
|
150
|
+
* a real no-op-by-default seam: absent, the scaffold uses today's behavior; none
|
|
151
|
+
* fabricates a result.
|
|
152
|
+
*/
|
|
153
|
+
export interface WizardHooks {
|
|
154
|
+
/**
|
|
155
|
+
* RUSH-2220 — model catalog pick. Given the resolved host (+ version and the
|
|
156
|
+
* current value in edit), return a chosen model id, or `null` to fall through
|
|
157
|
+
* to the free-text prompt. Absent → always free-text (today's behavior).
|
|
158
|
+
*/
|
|
159
|
+
pickModel?: (io: WizardIO, host: AgentId | undefined, version: string | undefined, current: string | undefined) => Promise<string | null>;
|
|
160
|
+
/**
|
|
161
|
+
* RUSH-2221 — connection test after configure, before save. Absent → no test
|
|
162
|
+
* (the wizard saves without one, exactly as today).
|
|
163
|
+
*/
|
|
164
|
+
connectionTest?: (draft: HarnessDraft) => Promise<ConnectionTestResult>;
|
|
165
|
+
/**
|
|
166
|
+
* RUSH-2222 — per-host editability matrix. Absent → {@link defaultEditable}.
|
|
167
|
+
*/
|
|
168
|
+
editable?: (host: AgentId) => HarnessEditable;
|
|
169
|
+
}
|
|
170
|
+
/** Resolve the host CLI a fork `source` runs under (the host `buildFork` will use). */
|
|
171
|
+
export declare function hostForSource(source: string | undefined): AgentId | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* The engine. Walk the steps in order; for each, ask `decide` what to do, then
|
|
174
|
+
* prompt only when it says `'run'`. A `{ disabled }` decision surfaces the reason
|
|
175
|
+
* and moves on; `'skip'` is silent. Returns the finished draft.
|
|
176
|
+
*/
|
|
177
|
+
export declare function runWizardSteps(steps: WizardStep[], draft: HarnessDraft, io: WizardIO, hooks?: WizardHooks): Promise<HarnessDraft>;
|
|
178
|
+
/**
|
|
179
|
+
* The create step list — the shape of `agents harness add`/`fork`. Faithful to
|
|
180
|
+
* the previous `runHarnessWizard` sequence (source → preset|custom → model →
|
|
181
|
+
* provider → base URL → name → key source), re-expressed as engine steps so the
|
|
182
|
+
* sibling subtasks can gate/replace individual steps. Produces the same
|
|
183
|
+
* `{ source, name, opts }` draft the fork flow already persists.
|
|
184
|
+
*/
|
|
185
|
+
export declare function createSteps(): WizardStep[];
|
|
186
|
+
/**
|
|
187
|
+
* The edit step list — `agents harness edit <name>` with no flags on a TTY. Each
|
|
188
|
+
* step is pre-filled with the profile's current value and gated by the host's
|
|
189
|
+
* editability matrix (RUSH-2222 seam): an unsupported param reads as disabled with
|
|
190
|
+
* a reason instead of being silently accepted. Records into the same draft, which
|
|
191
|
+
* `runEditWizard` maps to the `EditOptions` shape the flag path already persists.
|
|
192
|
+
*/
|
|
193
|
+
export declare function editSteps(original: Profile): WizardStep[];
|
|
194
|
+
/**
|
|
195
|
+
* Run the connection-test hook against a finished draft, if one is wired
|
|
196
|
+
* (RUSH-2221). Returns `null` when no hook is present (no test performed) so the
|
|
197
|
+
* caller can distinguish "not tested" from "tested and passed". Separated from the
|
|
198
|
+
* step list because the test needs the assembled profile, which the caller builds.
|
|
199
|
+
*/
|
|
200
|
+
export declare function runConnectionTest(draft: HarnessDraft, hooks: WizardHooks): Promise<ConnectionTestResult | null>;
|
|
201
|
+
/**
|
|
202
|
+
* Production {@link WizardIO} over `@inquirer/prompts`, lazy-imported so the
|
|
203
|
+
* dependency loads only when a wizard actually runs. `note` prints to stderr so it
|
|
204
|
+
* never contaminates a `--json`/piped stdout.
|
|
205
|
+
*/
|
|
206
|
+
export declare function defaultWizardIO(): Promise<WizardIO>;
|