@gr8ful/spf 0.6.0 → 0.8.0
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 +122 -27
- package/assets/prompts/refiner/system.md +11 -1
- package/assets/prompts/refiner/user.md +9 -3
- package/assets/skill/references/config.md +51 -13
- package/assets/templates/ts.spf.config.yaml +6 -2
- package/dist/chains/context.d.ts +26 -0
- package/dist/chains/simple_sdlc.js +9 -0
- package/dist/chains/steps.d.ts +0 -27
- package/dist/chains/steps.js +21 -2
- package/dist/cli/ask.d.ts +13 -0
- package/dist/cli/ask.js +15 -1
- package/dist/cli/commands/doctor.js +47 -9
- package/dist/cli/commands/fanout.js +49 -5
- package/dist/cli/commands/init.js +11 -3
- package/dist/cli/commands/list.d.ts +1 -1
- package/dist/cli/commands/list.js +31 -12
- package/dist/cli/commands/phases.d.ts +1 -1
- package/dist/cli/commands/phases.js +18 -4
- package/dist/cli/commands/run.js +30 -2
- package/dist/cli/commands/sessions.d.ts +1 -1
- package/dist/cli/commands/sessions.js +11 -3
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +93 -13
- package/dist/cli/index.js +4 -4
- package/dist/cli/interview.js +9 -5
- package/dist/cli/ui/fanout_dashboard.d.ts +22 -0
- package/dist/cli/ui/fanout_dashboard.js +102 -0
- package/dist/cli/ui/ink_asker.d.ts +13 -0
- package/dist/cli/ui/ink_asker.js +247 -0
- package/dist/cli/ui/reports.d.ts +30 -0
- package/dist/cli/ui/reports.js +61 -0
- package/dist/cli/ui/run_dashboard.d.ts +15 -0
- package/dist/cli/ui/run_dashboard.js +131 -0
- package/dist/cli/ui/watch_dashboard.d.ts +22 -0
- package/dist/cli/ui/watch_dashboard.js +78 -0
- package/dist/core/console.d.ts +40 -1
- package/dist/core/console.js +25 -3
- package/dist/core/data_types.d.ts +108 -5
- package/dist/core/data_types.js +50 -5
- package/dist/core/fanout.d.ts +9 -0
- package/dist/core/fanout.js +6 -2
- package/dist/core/gates.js +24 -1
- package/dist/core/issues/github_provider.d.ts +39 -5
- package/dist/core/issues/github_provider.js +103 -4
- package/dist/core/issues/jira_provider.d.ts +79 -12
- package/dist/core/issues/jira_provider.js +97 -2
- package/dist/core/issues/provider.d.ts +73 -19
- package/dist/core/issues/provider.js +24 -7
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/refine.d.ts +45 -8
- package/dist/core/refine.js +98 -24
- package/dist/core/runner.d.ts +5 -1
- package/dist/core/runner.js +2 -1
- package/dist/core/session.d.ts +7 -1
- package/dist/core/session.js +5 -1
- package/dist/core/watch.d.ts +86 -3
- package/dist/core/watch.js +353 -29
- package/package.json +6 -1
|
@@ -16,11 +16,13 @@ import { isRepoAt, makeGit } from "../../core/git_helper.js";
|
|
|
16
16
|
import { GitHubProvider } from "../../core/issues/github_provider.js";
|
|
17
17
|
import { JiraProvider } from "../../core/issues/jira_provider.js";
|
|
18
18
|
import { BitbucketProvider } from "../../core/issues/bitbucket_provider.js";
|
|
19
|
+
import { isAuthoringProvider } from "../../core/issues/provider.js";
|
|
19
20
|
import { createWatchState, tick } from "../../core/watch.js";
|
|
20
21
|
import { findChain, resolveRequiredAgents, runChain as runChainDef } from "../../chains/index.js";
|
|
21
22
|
import { ReviewOutput } from "../../core/data_types.js";
|
|
22
23
|
import { SfDb } from "../../ui/server/db.js";
|
|
23
24
|
import { parseCli } from "../../core/utils.js";
|
|
25
|
+
import { isInteractive } from "../ask.js";
|
|
24
26
|
/** Kept well under Slack's own 2900-char slice on `detail` (see `slack_channel.ts`) — a reviewer can emit a lot of findings, but the PR body/notification only needs enough to tell a human whether to look closer. */
|
|
25
27
|
const MAX_REVIEW_DIGEST_CHARS = 1200;
|
|
26
28
|
/**
|
|
@@ -107,7 +109,7 @@ function resolveIssueProvider(cfg) {
|
|
|
107
109
|
console.error('JIRA_EMAIL and JIRA_API_TOKEN must both be set — spf watch needs an Atlassian account email plus an API token (id.atlassian.com -> Security -> API tokens). See README.md\'s "spf watch" section.');
|
|
108
110
|
return null;
|
|
109
111
|
}
|
|
110
|
-
return new JiraProvider(cfg.watch.jira.base_url, cfg.watch.jira.project_key, cfg.watch.label_prefix, email, token);
|
|
112
|
+
return new JiraProvider(cfg.watch.jira.base_url, cfg.watch.jira.project_key, cfg.watch.label_prefix, email, token, cfg.watch.jira.issue_types);
|
|
111
113
|
}
|
|
112
114
|
console.error(`watch.issue_provider ${JSON.stringify(cfg.watch.issue_provider)} is not supported`);
|
|
113
115
|
return null;
|
|
@@ -177,6 +179,14 @@ function releaseLock(lockPath) {
|
|
|
177
179
|
* machine needs, with sensible colors/descriptions. Doesn't touch git or
|
|
178
180
|
* run anything, so it skips watchCommand's repo/chain checks entirely —
|
|
179
181
|
* you can seed labels before ever wiring up a worktree-capable checkout.
|
|
182
|
+
*
|
|
183
|
+
* On Jira, with `watch.refine.enabled`, this also runs a READ-ONLY check of
|
|
184
|
+
* `watch.jira.issue_types` against the real project — the same check
|
|
185
|
+
* `watchCommand`'s own startup gate runs, exposed here too so a bad mapping
|
|
186
|
+
* can be caught (and fixed) before ever starting the daemon, not just at
|
|
187
|
+
* startup time. Labels themselves stay a pure report on Jira either way
|
|
188
|
+
* (Jira labels are freeform strings with no color/description registry to
|
|
189
|
+
* seed — see `jira_provider.ts`'s module comment).
|
|
180
190
|
*/
|
|
181
191
|
export async function watchInitCommand(argv) {
|
|
182
192
|
const { options } = parseCli(argv, ["cwd", "config"], []);
|
|
@@ -193,6 +203,15 @@ export async function watchInitCommand(argv) {
|
|
|
193
203
|
console.log(` ~ ${name} (updated color/description)`);
|
|
194
204
|
for (const name of result.unchanged)
|
|
195
205
|
console.log(` = ${name} (already correct)`);
|
|
206
|
+
if (cfg.watch.refine.enabled && cfg.watch.issue_provider === "jira" && provider instanceof JiraProvider) {
|
|
207
|
+
const checks = await provider.validateIssueTypes();
|
|
208
|
+
console.log(`\nvalidating watch.jira.issue_types against ${cfg.watch.jira.project_key}:`);
|
|
209
|
+
for (const c of checks) {
|
|
210
|
+
console.log(` ${c.exists ? "✓" : "✗"} ${c.kind} → ${c.jiraType}${c.exists ? "" : ` — no issue type named "${c.jiraType}" in ${cfg.watch.jira.project_key}`}`);
|
|
211
|
+
}
|
|
212
|
+
if (checks.some((c) => !c.exists))
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
196
215
|
return 0;
|
|
197
216
|
}
|
|
198
217
|
export async function watchCommand(argv) {
|
|
@@ -215,19 +234,37 @@ export async function watchCommand(argv) {
|
|
|
215
234
|
return 1;
|
|
216
235
|
}
|
|
217
236
|
if (cfg.watch.refine.enabled) {
|
|
218
|
-
if (cfg.watch.issue_provider !== "github") {
|
|
219
|
-
// Fail loudly at startup, not silently every tick:
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
237
|
+
if (cfg.watch.issue_provider !== "github" && cfg.watch.issue_provider !== "jira") {
|
|
238
|
+
// Fail loudly at startup, not silently every tick: neither provider
|
|
239
|
+
// implements IssueAuthoringProvider besides these two — a refine lane
|
|
240
|
+
// that can never publish would otherwise just claim every spec-ready
|
|
241
|
+
// spec and block it, forever.
|
|
223
242
|
console.error(`watch.refine.enabled is true but watch.issue_provider is ${JSON.stringify(cfg.watch.issue_provider)} — ` +
|
|
224
|
-
`the refine lane needs "github"
|
|
243
|
+
`the refine lane needs "github" or "jira"`);
|
|
225
244
|
return 1;
|
|
226
245
|
}
|
|
227
246
|
if (!findChain(cfg.watch.refine.chain)) {
|
|
228
247
|
console.error(`watch.refine.chain ${JSON.stringify(cfg.watch.refine.chain)} is not a registered chain — run \`spf list\` to see every chain`);
|
|
229
248
|
return 1;
|
|
230
249
|
}
|
|
250
|
+
// Jira's issue-type mapping is user-configured (watch.jira.issue_types)
|
|
251
|
+
// and project-specific — validated here, at startup, for the same
|
|
252
|
+
// reason findChain() is: a bad mapping should stop the daemon before
|
|
253
|
+
// it starts, not fail silently every tick once the first spec-ready
|
|
254
|
+
// spec tries to publish. Silent on success, matching this function's
|
|
255
|
+
// other startup checks; loud (and the full per-kind report) on failure.
|
|
256
|
+
if (cfg.watch.issue_provider === "jira" && provider instanceof JiraProvider) {
|
|
257
|
+
const checks = await provider.validateIssueTypes();
|
|
258
|
+
const mismatches = checks.filter((c) => !c.exists);
|
|
259
|
+
if (mismatches.length > 0) {
|
|
260
|
+
console.error(`watch.jira.issue_types has ${mismatches.length} mismatch(es) against ${cfg.watch.jira.project_key}:`);
|
|
261
|
+
for (const c of checks) {
|
|
262
|
+
console.error(` ${c.exists ? "✓" : "✗"} ${c.kind} → ${c.jiraType}${c.exists ? "" : ` — no issue type named "${c.jiraType}" in ${cfg.watch.jira.project_key}`}`);
|
|
263
|
+
}
|
|
264
|
+
console.error(`Fix watch.jira.issue_types, or the project's issue types, before running spf watch unattended. Run \`spf watch init\` any time to re-check.`);
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
231
268
|
}
|
|
232
269
|
const dataPaths = paths.resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db);
|
|
233
270
|
const lockPath = path.join(dataPaths.data_dir, "watch.lock");
|
|
@@ -422,6 +459,34 @@ export async function watchCommand(argv) {
|
|
|
422
459
|
}
|
|
423
460
|
return { accepted: true, adwId: opts.adwId, detail: "", created, questions };
|
|
424
461
|
};
|
|
462
|
+
// `IssueAuthoringProvider`'s read-back half — `isAuthoringProvider()` is a
|
|
463
|
+
// structural check (see `provider.ts`), so both GitHub and Jira are
|
|
464
|
+
// recognized here without an `instanceof` chain that would need editing
|
|
465
|
+
// for every future authoring-capable provider. `null` on any tracker that
|
|
466
|
+
// ISN'T authoring-capable makes `rollUp` a logged no-op there rather than
|
|
467
|
+
// a startup failure the way `watch.refine.enabled` without authoring
|
|
468
|
+
// support is above: the build lane functions fine without container
|
|
469
|
+
// roll-up, unlike refine, which cannot function without authoring at all.
|
|
470
|
+
// Not a second `resolveAuthoringProvider()` call: that helper's own config
|
|
471
|
+
// validation already ran to produce `provider` itself.
|
|
472
|
+
const authoringProvider = isAuthoringProvider(provider) ? provider : null;
|
|
473
|
+
// Only for the one dispatch a human is actually watching a terminal for —
|
|
474
|
+
// `core/watch.ts` itself is untouched; this only ever swaps `deps.log`
|
|
475
|
+
// below and reads `state.inflight`/`state.refining` sizes after each
|
|
476
|
+
// `tick()`, exactly the seam the plan called for.
|
|
477
|
+
let dashboard;
|
|
478
|
+
if (isInteractive()) {
|
|
479
|
+
const { mountWatchDashboard } = await import("../ui/watch_dashboard.js");
|
|
480
|
+
dashboard = mountWatchDashboard({
|
|
481
|
+
repo: `${cfg.watch.issue_provider}+${cfg.watch.code_host} ${cfg.watch.repo}`,
|
|
482
|
+
labelPrefix: cfg.watch.label_prefix,
|
|
483
|
+
chain: cfg.watch.chain,
|
|
484
|
+
concurrency: cfg.watch.concurrency,
|
|
485
|
+
refineChain: cfg.watch.refine.enabled ? cfg.watch.refine.chain : undefined,
|
|
486
|
+
refineConcurrency: cfg.watch.refine.enabled ? cfg.watch.refine.concurrency : undefined,
|
|
487
|
+
dryRun: Boolean(flags["dry-run"]),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
425
490
|
const deps = {
|
|
426
491
|
provider,
|
|
427
492
|
codeHost,
|
|
@@ -440,8 +505,12 @@ export async function watchCommand(argv) {
|
|
|
440
505
|
linkDataDir,
|
|
441
506
|
dryRun: Boolean(flags["dry-run"]),
|
|
442
507
|
runChain,
|
|
443
|
-
|
|
444
|
-
|
|
508
|
+
listChildren: authoringProvider ? (parent) => authoringProvider.listChildren(parent) : undefined,
|
|
509
|
+
log: (message) => (dashboard ? dashboard.log(message) : console.log(message)),
|
|
510
|
+
notify: (event) => {
|
|
511
|
+
notifier?.send(event); // unaffected either way — see mountWatchDashboard's own doc comment
|
|
512
|
+
dashboard?.mirrorNotify(event);
|
|
513
|
+
},
|
|
445
514
|
};
|
|
446
515
|
const state = createWatchState();
|
|
447
516
|
let stopping = false;
|
|
@@ -465,6 +534,7 @@ export async function watchCommand(argv) {
|
|
|
465
534
|
interrupt?.();
|
|
466
535
|
sigints++;
|
|
467
536
|
if (sigints >= 2) {
|
|
537
|
+
dashboard?.unmountNow(); // best-effort cursor-visibility restore before the abrupt exit — see WatchDashboard.unmountNow's own comment
|
|
468
538
|
console.error("\n[spf] watch second interrupt — exiting immediately, without draining");
|
|
469
539
|
releaseLock(lockPath);
|
|
470
540
|
process.exit(130);
|
|
@@ -475,9 +545,15 @@ export async function watchCommand(argv) {
|
|
|
475
545
|
};
|
|
476
546
|
process.on("SIGINT", stop);
|
|
477
547
|
process.on("SIGTERM", stop);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
548
|
+
// The dashboard already renders this exact information as its own live
|
|
549
|
+
// header (repo/label/chain/concurrency/dry-run) — printing it again here
|
|
550
|
+
// would both duplicate it and interleave a raw console.log with an
|
|
551
|
+
// active Ink region.
|
|
552
|
+
if (!dashboard) {
|
|
553
|
+
console.log(`[spf] watch ${cfg.watch.issue_provider}+${cfg.watch.code_host} ${cfg.watch.repo} label "${cfg.watch.label_prefix}:*" chain "${cfg.watch.chain}" concurrency ${cfg.watch.concurrency}` +
|
|
554
|
+
(cfg.watch.refine.enabled ? ` refine "${cfg.watch.refine.chain}" concurrency ${cfg.watch.refine.concurrency}` : "") +
|
|
555
|
+
(flags["dry-run"] ? " (dry run)" : ""));
|
|
556
|
+
}
|
|
481
557
|
deps.notify({
|
|
482
558
|
kind: "watch_started",
|
|
483
559
|
level: "info",
|
|
@@ -490,15 +566,18 @@ export async function watchCommand(argv) {
|
|
|
490
566
|
});
|
|
491
567
|
try {
|
|
492
568
|
for (;;) {
|
|
569
|
+
dashboard?.setNextPollAt(null); // clears any stale countdown while a tick is actually running
|
|
493
570
|
await tick(deps, state);
|
|
571
|
+
dashboard?.setCounts(state.inflight.size, state.refining.size);
|
|
494
572
|
if (flags["once"] || stopping)
|
|
495
573
|
break;
|
|
574
|
+
dashboard?.setNextPollAt(Date.now() + cfg.watch.poll_ms);
|
|
496
575
|
await interruptibleSleep(cfg.watch.poll_ms);
|
|
497
576
|
if (stopping)
|
|
498
577
|
break;
|
|
499
578
|
}
|
|
500
579
|
while (state.inflight.size > 0) {
|
|
501
|
-
|
|
580
|
+
deps.log(`[spf] watch draining ${state.inflight.size} in-flight issue(s)...`); // deps.log already routes to the dashboard when one is mounted, console.log otherwise
|
|
502
581
|
await interruptibleSleep(1000);
|
|
503
582
|
if (stopping && sigints >= 2)
|
|
504
583
|
break; // stop() itself already exits on the 2nd signal; this is belt-and-suspenders
|
|
@@ -511,5 +590,6 @@ export async function watchCommand(argv) {
|
|
|
511
590
|
process.off("SIGINT", stop);
|
|
512
591
|
process.off("SIGTERM", stop);
|
|
513
592
|
releaseLock(lockPath);
|
|
593
|
+
await dashboard?.close();
|
|
514
594
|
}
|
|
515
595
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -49,7 +49,7 @@ const HELP = `spf — repeatable agents-plus-code workflows (ADWs)
|
|
|
49
49
|
spf abort <adw_id> signal a run's process to stop
|
|
50
50
|
spf version print the installed version
|
|
51
51
|
|
|
52
|
-
Chain options: [--config <path>] [--adw-id <id>] [--cwd <dir>] [--agent <name>] [--base <ref>] [--issue <id>]
|
|
52
|
+
Chain options: [--config <path>] [--adw-id <id>] [--cwd <dir>] [--agent <name>] [--base <ref>] [--issue <id>] [--priority p0|p1|p2|p3]
|
|
53
53
|
Run budget: set defaults.max_run_cost (USD) and/or defaults.max_run_tokens in spf.config.yaml to stop the NEXT agent call once a run has already spent this much — checked before each call, never after, so a single call is never capped and a one-agent-dispatch chain (scout/prompt/build) can never trip it; absent (the default) = unbounded.
|
|
54
54
|
Run \`spf list\` to see every chain and what it needs.`;
|
|
55
55
|
/** A raw scan for `--cwd`, ahead of any command-specific argv parsing — every command that takes it means the same thing by it. */
|
|
@@ -151,7 +151,7 @@ export async function main() {
|
|
|
151
151
|
process.exitCode = await estimateCommand(rest);
|
|
152
152
|
return;
|
|
153
153
|
case "list":
|
|
154
|
-
process.exitCode = listCommand();
|
|
154
|
+
process.exitCode = await listCommand();
|
|
155
155
|
return;
|
|
156
156
|
case "init":
|
|
157
157
|
process.exitCode = await initCommand(rest);
|
|
@@ -177,10 +177,10 @@ export async function main() {
|
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
179
|
case "sessions":
|
|
180
|
-
process.exitCode = sessionsCommand(rest);
|
|
180
|
+
process.exitCode = await sessionsCommand(rest);
|
|
181
181
|
return;
|
|
182
182
|
case "phases":
|
|
183
|
-
process.exitCode = phasesCommand(rest);
|
|
183
|
+
process.exitCode = await phasesCommand(rest);
|
|
184
184
|
return;
|
|
185
185
|
case "events":
|
|
186
186
|
process.exitCode = await eventsCommand(rest);
|
package/dist/cli/interview.js
CHANGED
|
@@ -286,16 +286,20 @@ export async function runInterview(asker, ctx) {
|
|
|
286
286
|
watch = { issue_provider: issueProvider, code_host: codeHost, repo, label_prefix: labelPrefix, chain, base_branch: baseBranch };
|
|
287
287
|
if (issueRepo)
|
|
288
288
|
watch.issue_repo = issueRepo;
|
|
289
|
-
// Issue authoring (create + link a hierarchy) is
|
|
290
|
-
// GitHubProvider
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
if (issueProvider === "github") {
|
|
289
|
+
// Issue authoring (create + link a hierarchy) is implemented on both
|
|
290
|
+
// GitHubProvider and JiraProvider — see jira_provider.ts's module
|
|
291
|
+
// comment for how Jira's version works (native issue types + the
|
|
292
|
+
// `parent` field).
|
|
293
|
+
if (issueProvider === "github" || issueProvider === "jira") {
|
|
294
294
|
const enableRefine = await asker.confirm(`Also enable the refine lane (decompose a "${labelPrefix}:spec-ready" product spec into a feature/story-or-bug tree)?`, false);
|
|
295
295
|
if (enableRefine) {
|
|
296
296
|
const refineChainChoices = allChains().map((c) => ({ value: c.name, label: c.name, hint: c.source ? `${c.describe} (repo)` : c.describe }));
|
|
297
297
|
const refineChain = await asker.select("Chain to run per spec", refineChainChoices, "refine");
|
|
298
298
|
watch.refine = { enabled: true, chain: refineChain };
|
|
299
|
+
if (issueProvider === "jira") {
|
|
300
|
+
asker.note('Jira issue types default to epic/feature -> "Epic", story -> "Story", bug -> "Bug", task -> "Task" — ' +
|
|
301
|
+
"customize per-kind in watch.jira.issue_types if your project renames any of them, then run `spf watch init` to validate.");
|
|
302
|
+
}
|
|
299
303
|
}
|
|
300
304
|
}
|
|
301
305
|
if (issueProvider === "jira") {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FanoutAttempt } from "../../core/fanout.ts";
|
|
2
|
+
export interface FanoutDashboard {
|
|
3
|
+
onAttempt: (attempt: FanoutAttempt) => void;
|
|
4
|
+
/** Routes `FanoutDeps.log()` here instead of `console.log` while this is mounted — see the module header for why a raw console write can't share the terminal with an active Ink instance. */
|
|
5
|
+
log: (message: string) => void;
|
|
6
|
+
/** Tears the Ink instance down. Call exactly once, after `runBestOf()` settles — the plain-text winner/basis lines after it are unaffected; only the redundant header + final table are skipped when a dashboard was used (see `cli/commands/fanout.ts`'s call site). */
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Synchronous, no flush-wait — for a signal handler, which stays
|
|
10
|
+
* synchronous by design and gets cut off by `process.exit()` immediately
|
|
11
|
+
* after: an `await` here would never get a chance to resume. Only
|
|
12
|
+
* restores terminal state (cursor visibility) on a best-effort basis; the
|
|
13
|
+
* final frame may not have caught up to the last attempt yet.
|
|
14
|
+
*/
|
|
15
|
+
unmountNow(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function mountFanoutDashboard(opts: {
|
|
18
|
+
n: number;
|
|
19
|
+
chainName: string;
|
|
20
|
+
baseBranch: string;
|
|
21
|
+
baseAdwId: string;
|
|
22
|
+
}): FanoutDashboard;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A live results table for `spf fanout` — the same columns
|
|
4
|
+
* `cli/commands/fanout.ts`'s `printTable()` prints once at the end, updated
|
|
5
|
+
* row by row as each attempt settles instead. Reached only through a
|
|
6
|
+
* dynamic `import()` and only when `isInteractive()`; non-TTY keeps the
|
|
7
|
+
* exact one-shot `printTable()` path, byte for byte (`fanout_cli.test.ts`
|
|
8
|
+
* runs with no TTY, so it never touches this file).
|
|
9
|
+
*
|
|
10
|
+
* One persistent Ink instance for the whole `runBestOf()` call — same shape
|
|
11
|
+
* as `run_dashboard.tsx`: a `<Static>` history for `deps.log()`'s own
|
|
12
|
+
* lines (the "chain X x3 off main" opener, an "attempt N skipped" line —
|
|
13
|
+
* `cli/commands/fanout.ts` routes these here instead of straight to
|
|
14
|
+
* `console.log` specifically so nothing writes to stdout outside Ink while
|
|
15
|
+
* this is mounted; interleaving a raw `console.log` with an active Ink
|
|
16
|
+
* live region corrupts the redraw) plus a LIVE section below it — here,
|
|
17
|
+
* the whole results table, not one line, since every row can still change
|
|
18
|
+
* until the very last attempt settles and there's no "this phase is done"
|
|
19
|
+
* moment to freeze one into history the way `run_dashboard.tsx` does per
|
|
20
|
+
* phase. Nothing here calls `useInput`, so raw mode never engages.
|
|
21
|
+
*/
|
|
22
|
+
import { useState, useRef } from "react";
|
|
23
|
+
import { render, Static, Box, Text } from "ink";
|
|
24
|
+
import * as agents from "../../core/agents.js";
|
|
25
|
+
import { attemptAdwId, attemptBranch } from "../../core/fanout.js";
|
|
26
|
+
function statusLabel(row) {
|
|
27
|
+
if (!row)
|
|
28
|
+
return "running…";
|
|
29
|
+
return row.status;
|
|
30
|
+
}
|
|
31
|
+
function gatesLabel(row) {
|
|
32
|
+
if (!row || row.status === "skipped")
|
|
33
|
+
return "-";
|
|
34
|
+
return `${row.gate_passes}p/${row.gate_failures}f`;
|
|
35
|
+
}
|
|
36
|
+
function timeLabel(row) {
|
|
37
|
+
if (!row || row.status === "skipped")
|
|
38
|
+
return "-";
|
|
39
|
+
const seconds = row.wall_ms / 1000;
|
|
40
|
+
return seconds < 90 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${String(Math.round(seconds % 60)).padStart(2, "0")}s`;
|
|
41
|
+
}
|
|
42
|
+
function rowColor(row) {
|
|
43
|
+
if (!row)
|
|
44
|
+
return "dim";
|
|
45
|
+
if (row.status === "success")
|
|
46
|
+
return "green";
|
|
47
|
+
if (row.status === "fail" || row.status === "error")
|
|
48
|
+
return "red";
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function FanoutRoot(props) {
|
|
52
|
+
const [rows, setRows] = useState(new Map());
|
|
53
|
+
const [log, setLog] = useState([]);
|
|
54
|
+
const keyRef = useRef(0); // a ref, not state — see ink_asker.tsx's identical comment on why two pushes in one tick must not collide
|
|
55
|
+
props.handleRef.current = {
|
|
56
|
+
setAttempt(attempt) {
|
|
57
|
+
setRows((prev) => {
|
|
58
|
+
const next = new Map(prev);
|
|
59
|
+
next.set(attempt.index, attempt);
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
pushLog(text) {
|
|
64
|
+
const key = keyRef.current++;
|
|
65
|
+
setLog((prev) => [...prev, { key, text }]);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const indices = Array.from({ length: props.n }, (_, i) => i + 1);
|
|
69
|
+
const widths = {
|
|
70
|
+
adw: Math.max(6, ...indices.map((i) => attemptAdwId(props.baseAdwId, i).length)),
|
|
71
|
+
branch: Math.max(6, ...indices.map((i) => attemptBranch(props.baseAdwId, i).length)),
|
|
72
|
+
};
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: log, children: (line) => _jsx(Text, { children: line.text }, line.key) }), _jsxs(Text, { children: ["fanout ", props.chainName, " \u2014 ", props.n, " attempt(s), base ", props.baseBranch] }), _jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(Text, { dimColor: true, children: "#" }) }), _jsx(Box, { width: widths.adw, marginRight: 2, children: _jsx(Text, { dimColor: true, children: "adw_id" }) }), _jsx(Box, { width: widths.branch, marginRight: 2, children: _jsx(Text, { dimColor: true, children: "branch" }) }), _jsx(Box, { width: 9, children: _jsx(Text, { dimColor: true, children: "status" }) }), _jsx(Box, { width: 9, children: _jsx(Text, { dimColor: true, children: "gates" }) }), _jsx(Box, { width: 10, children: _jsx(Text, { dimColor: true, children: "cost" }) }), _jsx(Text, { dimColor: true, children: "time" })] }), indices.map((i) => {
|
|
74
|
+
const row = rows.get(i) ?? null;
|
|
75
|
+
const color = rowColor(row);
|
|
76
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, children: _jsx(Text, { color: color, children: i }) }), _jsx(Box, { width: widths.adw, marginRight: 2, children: _jsx(Text, { color: color, children: attemptAdwId(props.baseAdwId, i) }) }), _jsx(Box, { width: widths.branch, marginRight: 2, children: _jsx(Text, { color: color, children: attemptBranch(props.baseAdwId, i) }) }), _jsx(Box, { width: 9, children: _jsx(Text, { color: color, children: statusLabel(row) }) }), _jsx(Box, { width: 9, children: _jsx(Text, { color: color, children: gatesLabel(row) }) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: color, children: row ? agents.formatUsd(row.cost) : "-" }) }), _jsx(Text, { color: color, children: timeLabel(row) })] }, i));
|
|
77
|
+
})] }));
|
|
78
|
+
}
|
|
79
|
+
export function mountFanoutDashboard(opts) {
|
|
80
|
+
const handleRef = { current: null };
|
|
81
|
+
const app = render(_jsx(FanoutRoot, { n: opts.n, chainName: opts.chainName, baseBranch: opts.baseBranch, baseAdwId: opts.baseAdwId, handleRef: handleRef }),
|
|
82
|
+
// `interactive: true` overrides Ink's own CI auto-detection — see
|
|
83
|
+
// `ink_asker.tsx`'s identical `render()` call for why: the caller
|
|
84
|
+
// (`commands/fanout.ts`) only reaches this file after its own
|
|
85
|
+
// `isInteractive()` check has already passed.
|
|
86
|
+
{ patchConsole: false, interactive: true });
|
|
87
|
+
return {
|
|
88
|
+
onAttempt(attempt) {
|
|
89
|
+
handleRef.current?.setAttempt(attempt);
|
|
90
|
+
},
|
|
91
|
+
log(message) {
|
|
92
|
+
handleRef.current?.pushLog(message);
|
|
93
|
+
},
|
|
94
|
+
async close() {
|
|
95
|
+
await app.waitUntilRenderFlush();
|
|
96
|
+
app.unmount();
|
|
97
|
+
},
|
|
98
|
+
unmountNow() {
|
|
99
|
+
app.unmount();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Asker } from "../ask.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The real `stdin`/`stdout` pair the interview reads/writes — defaults to
|
|
4
|
+
* the process's own. `src/test/ink_asker.test.ts` passes a fake TTY pair
|
|
5
|
+
* instead (`node --test` has neither a real terminal nor raw-mode
|
|
6
|
+
* support), the same seam Ink's own render options already expose; this
|
|
7
|
+
* is not new surface, just threaded through so a test can reach it.
|
|
8
|
+
*/
|
|
9
|
+
export interface RenderStreams {
|
|
10
|
+
stdin?: NodeJS.ReadStream;
|
|
11
|
+
stdout?: NodeJS.WriteStream;
|
|
12
|
+
}
|
|
13
|
+
export declare function createInkAsker(streams?: RenderStreams): Asker;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Ink-backed implementation of the `Asker` interface (`cli/ask.ts`) — a
|
|
4
|
+
* drop-in replacement for `createAsker()`'s readline prompts, used only
|
|
5
|
+
* when `inkAvailable()` says raw-mode stdin is there to drive it.
|
|
6
|
+
* `runInterview()` (`cli/interview.ts`) never sees this file: it only ever
|
|
7
|
+
* imports the `Asker` type, so it is byte-for-byte unchanged by this
|
|
8
|
+
* module existing, and every `createFakeAsker`-driven test still exercises
|
|
9
|
+
* it exactly as before.
|
|
10
|
+
*
|
|
11
|
+
* This file is reached only through a dynamic `import()` (see
|
|
12
|
+
* `commands/init.ts`) so the ~150-300ms cost of loading `ink`+`react` is
|
|
13
|
+
* never paid on a non-interactive run.
|
|
14
|
+
*
|
|
15
|
+
* ONE Ink instance is mounted lazily on the first prompt and kept alive for
|
|
16
|
+
* the whole interview, torn down only in `close()`. An earlier version
|
|
17
|
+
* mounted a fresh instance per question instead — that toggled raw mode
|
|
18
|
+
* off (on unmount) and back on (on the next mount) between every single
|
|
19
|
+
* question, and empirically, over a real pty, a keystroke landing in that
|
|
20
|
+
* gap got echoed to the screen instead of consumed, and could be lost or
|
|
21
|
+
* misrouted entirely (arrow-key-then-Enter on the second question the
|
|
22
|
+
* default readline-vs-Ink smoke test tried was the one that caught it —
|
|
23
|
+
* the model select accepted the arrow move but the following Enter never
|
|
24
|
+
* registered). Keeping raw mode continuously enabled for the interview's
|
|
25
|
+
* entire lifetime removes the gap outright, and is also just the correct
|
|
26
|
+
* Ink pattern for a multi-step wizard: finished questions accumulate in an
|
|
27
|
+
* `<Static>` list (rendered once, never touched again — exactly the
|
|
28
|
+
* "leaves the final frame in scrollback" transcript this asker owes
|
|
29
|
+
* `runInterview()`) while the current question lives in one live slot
|
|
30
|
+
* below it, both children of the SAME root, updated by `rerender()`.
|
|
31
|
+
*
|
|
32
|
+
* `exitOnCtrlC: false`: `spf watch` and `core/session.ts` own SIGINT/
|
|
33
|
+
* SIGTERM handling, and Ink's default Ctrl-C behavior would call
|
|
34
|
+
* `process.exit()` out from under both. Ctrl-C (and Ctrl-D, treated the
|
|
35
|
+
* same way the readline asker treats EOF) is instead caught once, at the
|
|
36
|
+
* root, and turned into the same `InterviewAborted` the readline asker
|
|
37
|
+
* throws — `initCommand` catches it and exits 130, writing nothing.
|
|
38
|
+
*/
|
|
39
|
+
import { useRef, useState } from "react";
|
|
40
|
+
import { render, Static, Box, Text, useInput } from "ink";
|
|
41
|
+
import { ConfirmInput, PasswordInput, TextInput } from "@inkjs/ui";
|
|
42
|
+
import { InterviewAborted, maskForPrompt } from "../ask.js";
|
|
43
|
+
import { paint } from "../../core/console.js";
|
|
44
|
+
function InterviewRoot(props) {
|
|
45
|
+
const [history, setHistory] = useState([]);
|
|
46
|
+
const [prompt, setPrompt] = useState(null);
|
|
47
|
+
// A ref, not `useState` — two `pushHistory` calls can happen back to back
|
|
48
|
+
// synchronously (e.g. `heading()` immediately followed by `note()`, exactly
|
|
49
|
+
// how `runInterview()` opens each section) with no re-render in between to
|
|
50
|
+
// flush a `nextKey` state update, so both would read the same stale value
|
|
51
|
+
// and mint duplicate keys. A ref's mutation is immediate or it wouldn't be
|
|
52
|
+
// safe as a counter at all.
|
|
53
|
+
const keyRef = useRef(0);
|
|
54
|
+
props.handleRef.current = {
|
|
55
|
+
pushHistory(node) {
|
|
56
|
+
const key = keyRef.current++;
|
|
57
|
+
setHistory((h) => [...h, { key, node }]);
|
|
58
|
+
},
|
|
59
|
+
setPrompt,
|
|
60
|
+
};
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
if (key.ctrl && (input === "c" || input === "d"))
|
|
63
|
+
props.onAbort();
|
|
64
|
+
});
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (line) => _jsx(Box, { children: line.node }, line.key) }), prompt] }));
|
|
66
|
+
}
|
|
67
|
+
function TextPromptView(props) {
|
|
68
|
+
const [error, setError] = useState(null);
|
|
69
|
+
// `TextInput` is uncontrolled — `defaultValue` only seeds its FIRST value,
|
|
70
|
+
// so a rejected submit otherwise leaves the rejected text sitting in the
|
|
71
|
+
// box and whatever's typed next appends onto it instead of replacing it.
|
|
72
|
+
// Changing `key` forces React to unmount and remount a fresh instance
|
|
73
|
+
// (starting empty again) on every rejection, which is the standard way
|
|
74
|
+
// to "reset" an uncontrolled component.
|
|
75
|
+
const [attempt, setAttempt] = useState(0);
|
|
76
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: props.label }), props.defaultValue ? _jsxs(Text, { dimColor: true, children: [" [", props.defaultValue, "]"] }) : null, _jsx(Text, { children: ": " }), _jsx(TextInput, { defaultValue: props.defaultValue, onSubmit: (value) => {
|
|
77
|
+
const resolved = value || props.defaultValue || "";
|
|
78
|
+
const problem = props.validate?.(resolved) ?? null;
|
|
79
|
+
if (problem) {
|
|
80
|
+
setError(problem);
|
|
81
|
+
setAttempt((n) => n + 1);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
props.onSubmit(resolved);
|
|
85
|
+
} }, attempt)] }), error ? _jsxs(Text, { color: "red", children: [" ", error] }) : null] }));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Hand-rolled instead of `@inkjs/ui`'s `Select` — that component's
|
|
89
|
+
* `onChange` only fires when its internal `value` differs from
|
|
90
|
+
* `previousValue` (`use-select-state.js`'s reducer + effect), and accepting
|
|
91
|
+
* the already-highlighted default via an immediate Enter, with no arrow
|
|
92
|
+
* key pressed first, never changes that value. That's the single most
|
|
93
|
+
* common interaction with any select prompt (take the default), so the
|
|
94
|
+
* bug isn't an edge case: every `select()` call whose first keypress is
|
|
95
|
+
* Enter would hang forever. Confirmed empirically before writing this —
|
|
96
|
+
* the same prompt resolves instantly the moment an arrow key precedes
|
|
97
|
+
* Enter. `ConfirmInput`/`TextInput`/`PasswordInput` call their callbacks
|
|
98
|
+
* unconditionally from their own input handlers and don't share this bug,
|
|
99
|
+
* so they're untouched.
|
|
100
|
+
*/
|
|
101
|
+
function SelectPromptView(props) {
|
|
102
|
+
const dfltIndex = props.choices.findIndex((c) => c.value === props.dflt);
|
|
103
|
+
const [index, setIndex] = useState(dfltIndex === -1 ? 0 : dfltIndex);
|
|
104
|
+
useInput((_input, key) => {
|
|
105
|
+
if (key.downArrow)
|
|
106
|
+
setIndex((i) => Math.min(props.choices.length - 1, i + 1));
|
|
107
|
+
if (key.upArrow)
|
|
108
|
+
setIndex((i) => Math.max(0, i - 1));
|
|
109
|
+
if (key.return)
|
|
110
|
+
props.onSubmit(props.choices[index].value);
|
|
111
|
+
});
|
|
112
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: props.label }), props.choices.map((c, i) => {
|
|
113
|
+
const focused = i === index;
|
|
114
|
+
const hint = c.hint ? paint("dim", ` — ${c.hint}`) : "";
|
|
115
|
+
return (_jsxs(Text, { color: focused ? "cyan" : undefined, children: [focused ? "❯ " : " ", c.label ?? c.value, hint] }, c.value));
|
|
116
|
+
})] }));
|
|
117
|
+
}
|
|
118
|
+
function ConfirmPromptView(props) {
|
|
119
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { children: [props.label, " "] }), _jsx(ConfirmInput, { defaultChoice: props.dflt ? "confirm" : "cancel", onConfirm: () => props.onSubmit(true), onCancel: () => props.onSubmit(false) })] }));
|
|
120
|
+
}
|
|
121
|
+
function SecretPromptView(props) {
|
|
122
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: props.label }), props.current ? _jsxs(Text, { dimColor: true, children: [" [keep current: ", maskForPrompt(props.current), "]"] }) : null, _jsx(Text, { children: ": " }), _jsx(PasswordInput, { onSubmit: props.onSubmit })] }));
|
|
123
|
+
}
|
|
124
|
+
export function createInkAsker(streams = {}) {
|
|
125
|
+
// Mirrors `createAsker()`'s own `aborted` flag: once a Ctrl-C/Ctrl-D fires,
|
|
126
|
+
// every later call throws immediately instead of showing a prompt that
|
|
127
|
+
// would just get torn down again.
|
|
128
|
+
let aborted = false;
|
|
129
|
+
// Whichever prompt is currently live — `InterviewRoot`'s single Ctrl-C/
|
|
130
|
+
// Ctrl-D handler rejects THIS, whatever it is, since only one prompt is
|
|
131
|
+
// ever showing at a time.
|
|
132
|
+
let currentReject = null;
|
|
133
|
+
let app;
|
|
134
|
+
const handleRef = { current: null };
|
|
135
|
+
function handle() {
|
|
136
|
+
if (!app) {
|
|
137
|
+
app = render(_jsx(InterviewRoot, { handleRef: handleRef, onAbort: () => {
|
|
138
|
+
aborted = true;
|
|
139
|
+
currentReject?.(new InterviewAborted());
|
|
140
|
+
} }),
|
|
141
|
+
// `interactive: true` overrides Ink's own auto-detection
|
|
142
|
+
// (`stdout.isTTY` + the `is-in-ci` package) rather than relying on
|
|
143
|
+
// it: the caller here is `commands/init.ts`, which only ever
|
|
144
|
+
// reaches this file after `inkAvailable()` has ALREADY confirmed a
|
|
145
|
+
// real interactive terminal — that check is the one source of
|
|
146
|
+
// truth, and Ink's own CI detection is redundant at best. At
|
|
147
|
+
// worst it actively lies: GitHub Actions sets `CI=true`
|
|
148
|
+
// unconditionally, and `node --test` running there is exactly
|
|
149
|
+
// where this file's own test suite (`ink_asker.test.ts`) drives a
|
|
150
|
+
// real Ink instance against a fake TTY that reports `isTTY: true`
|
|
151
|
+
// — Ink's non-interactive mode then writes NOTHING incrementally
|
|
152
|
+
// (only the final frame, at unmount), so `stdout.frames` never
|
|
153
|
+
// grows and every keystroke-driven test hangs until its own
|
|
154
|
+
// timeout. Confirmed by reproducing the exact CI failure locally
|
|
155
|
+
// with `CI=1 node --test ...` before this fix, and confirming it
|
|
156
|
+
// disappears after.
|
|
157
|
+
{ ...streams, exitOnCtrlC: false, patchConsole: false, interactive: true });
|
|
158
|
+
}
|
|
159
|
+
// `InterviewRoot` populates this synchronously during its first render,
|
|
160
|
+
// which `render()` above has already forced to happen by the time it
|
|
161
|
+
// returns.
|
|
162
|
+
return handleRef.current;
|
|
163
|
+
}
|
|
164
|
+
async function guarded(run) {
|
|
165
|
+
if (aborted)
|
|
166
|
+
throw new InterviewAborted();
|
|
167
|
+
try {
|
|
168
|
+
return await run(handle());
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (error instanceof InterviewAborted)
|
|
172
|
+
aborted = true;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
currentReject = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
text(label, opts) {
|
|
181
|
+
return guarded((h) => new Promise((resolve, reject) => {
|
|
182
|
+
currentReject = reject;
|
|
183
|
+
h.setPrompt(_jsx(TextPromptView, { label: label, defaultValue: opts?.default, validate: opts?.validate, onSubmit: (value) => {
|
|
184
|
+
h.setPrompt(null);
|
|
185
|
+
h.pushHistory(_jsxs(Text, { children: [label, ": ", value] }));
|
|
186
|
+
resolve(value);
|
|
187
|
+
} }));
|
|
188
|
+
}));
|
|
189
|
+
},
|
|
190
|
+
select(label, choices, dflt) {
|
|
191
|
+
return guarded((h) => new Promise((resolve, reject) => {
|
|
192
|
+
currentReject = reject;
|
|
193
|
+
h.setPrompt(_jsx(SelectPromptView, { label: label, choices: choices, dflt: dflt, onSubmit: (value) => {
|
|
194
|
+
const chosen = choices.find((c) => c.value === value);
|
|
195
|
+
h.setPrompt(null);
|
|
196
|
+
h.pushHistory(_jsxs(Text, { children: [label, " ", _jsxs(Text, { color: "cyan", children: ["\u276F ", chosen?.label ?? value] })] }));
|
|
197
|
+
resolve(value);
|
|
198
|
+
} }));
|
|
199
|
+
}));
|
|
200
|
+
},
|
|
201
|
+
confirm(label, dflt, opts) {
|
|
202
|
+
return guarded((h) => new Promise((resolve, reject) => {
|
|
203
|
+
let settled = false;
|
|
204
|
+
let timer;
|
|
205
|
+
currentReject = reject;
|
|
206
|
+
const finish = (value, note) => {
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
209
|
+
settled = true;
|
|
210
|
+
if (timer)
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
h.setPrompt(null);
|
|
213
|
+
h.pushHistory(_jsxs(Text, { children: [label, " ", value ? "yes" : "no", note ? _jsxs(Text, { color: "yellow", children: [" (", note, ")"] }) : null] }));
|
|
214
|
+
resolve(value);
|
|
215
|
+
};
|
|
216
|
+
if (opts?.timeoutMs !== undefined) {
|
|
217
|
+
timer = setTimeout(() => finish(dflt, `timed out — using default (${dflt ? "yes" : "no"})`), opts.timeoutMs);
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
}
|
|
220
|
+
h.setPrompt(_jsx(ConfirmPromptView, { label: label, dflt: dflt, onSubmit: (value) => finish(value) }));
|
|
221
|
+
}));
|
|
222
|
+
},
|
|
223
|
+
secret(label, opts) {
|
|
224
|
+
return guarded((h) => new Promise((resolve, reject) => {
|
|
225
|
+
currentReject = reject;
|
|
226
|
+
h.setPrompt(_jsx(SecretPromptView, { label: label, current: opts?.current, onSubmit: (value) => {
|
|
227
|
+
h.setPrompt(null);
|
|
228
|
+
h.pushHistory(_jsxs(Text, { children: [label, ": ", value ? maskForPrompt(value) : "(unchanged)"] }));
|
|
229
|
+
resolve(value);
|
|
230
|
+
} }));
|
|
231
|
+
}));
|
|
232
|
+
},
|
|
233
|
+
note(text) {
|
|
234
|
+
if (aborted)
|
|
235
|
+
return;
|
|
236
|
+
handle().pushHistory(_jsxs(Text, { dimColor: true, children: [" ", text] }));
|
|
237
|
+
},
|
|
238
|
+
heading(text) {
|
|
239
|
+
if (aborted)
|
|
240
|
+
return;
|
|
241
|
+
handle().pushHistory(_jsxs(Text, { bold: true, color: "cyan", children: ["\u2500\u2500 ", text, " \u2500\u2500"] }));
|
|
242
|
+
},
|
|
243
|
+
close() {
|
|
244
|
+
app?.unmount();
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|