wp-typia 0.16.2 → 0.16.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wp-typia",
3
- "version": "0.16.2",
3
+ "version": "0.16.5",
4
4
  "description": "Canonical CLI package for wp-typia scaffolding and project workflows",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -65,10 +65,10 @@
65
65
  "@bunli/tui": "0.6.0",
66
66
  "@bunli/utils": "0.6.0",
67
67
  "@wp-typia/api-client": "^0.4.2",
68
- "@wp-typia/project-tools": "0.16.1",
68
+ "@wp-typia/project-tools": "0.16.4",
69
69
  "better-result": "^2.7.0",
70
- "react": "19.2.0",
71
- "react-dom": "19.2.0",
70
+ "react": "^19.2.5",
71
+ "react-dom": "^19.2.5",
72
72
  "zod": "4.3.6"
73
73
  },
74
74
  "devDependencies": {
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
 
6
6
  import { getAddBlockDefaults } from "../config";
7
7
  import { resolveBundledModuleHref } from "../render-loader";
8
- import { executeAddCommand, getAddWorkspaceBlockOptions } from "../runtime-bridge";
8
+ import { executeAddCommand } from "../runtime-bridge";
9
9
  import type { WpTypiaRenderArgs } from "./render-types";
10
10
  import { LazyFlow } from "../ui/lazy-flow";
11
11
 
@@ -94,7 +94,6 @@ export const addCommand = defineCommand({
94
94
  template:
95
95
  (args.flags.template as string | undefined) ?? config.template,
96
96
  },
97
- workspaceBlockOptions: getAddWorkspaceBlockOptions(args.cwd),
98
97
  },
99
98
  });
100
99
  },
@@ -1,6 +1,5 @@
1
1
  import { defineCommand } from "@bunli/core";
2
-
3
- import { getDoctorChecks, runDoctor } from "@wp-typia/project-tools";
2
+ import { executeDoctorCommand } from "../runtime-bridge";
4
3
 
5
4
  export const doctorCommand = defineCommand({
6
5
  description: "Run repository and project diagnostics.",
@@ -10,6 +9,7 @@ export const doctorCommand = defineCommand({
10
9
  args.agent ||
11
10
  Boolean(args.context?.store?.isAIAgent);
12
11
  if (prefersStructuredOutput) {
12
+ const { getDoctorChecks } = await import("@wp-typia/project-tools/cli-doctor");
13
13
  const checks = await getDoctorChecks(args.cwd);
14
14
  args.output({ checks });
15
15
  if (checks.some((check) => check.status === "fail")) {
@@ -17,7 +17,7 @@ export const doctorCommand = defineCommand({
17
17
  }
18
18
  return;
19
19
  }
20
- await runDoctor(args.cwd);
20
+ await executeDoctorCommand(args.cwd);
21
21
  },
22
22
  name: "doctor",
23
23
  });
@@ -3,43 +3,35 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import {
6
- createReadlinePrompt,
7
- formatRunScript,
8
- formatAddHelpText,
9
- formatMigrationHelpText,
10
6
  formatTemplateDetails,
11
7
  formatTemplateFeatures,
12
8
  formatTemplateSummary,
13
- getWorkspaceBlockSelectOptions,
14
9
  getTemplateById,
15
- getTemplateSelectOptions,
16
10
  listTemplates,
17
- parseMigrationArgs,
18
- tryResolveWorkspaceProject,
19
- runAddBindingSourceCommand,
20
- runAddBlockCommand,
21
- runAddHookedBlockCommand,
22
- runAddPatternCommand,
23
- runAddVariationCommand,
24
- runDoctor,
25
- runMigrationCommand,
26
- runScaffoldFlow,
27
- } from "@wp-typia/project-tools";
28
- import type { ReadlinePrompt } from "@wp-typia/project-tools";
11
+ } from "@wp-typia/project-tools/cli-templates";
12
+ import { formatRunScript } from "@wp-typia/project-tools/package-managers";
13
+ import { tryResolveWorkspaceProject } from "@wp-typia/project-tools/workspace-project";
14
+ import type { ReadlinePrompt } from "@wp-typia/project-tools/cli-prompt";
15
+ import type { AlternateBufferCompletionPayload } from "./ui/alternate-buffer-lifecycle";
29
16
 
30
17
  type CreateExecutionInput = {
31
18
  projectDir: string;
32
19
  cwd: string;
20
+ emitOutput?: boolean;
33
21
  flags: Record<string, unknown>;
34
22
  interactive?: boolean;
23
+ printLine?: PrintLine;
35
24
  prompt?: ReadlinePrompt;
25
+ warnLine?: PrintLine;
36
26
  };
37
27
 
38
28
  type AddExecutionInput = {
39
29
  cwd: string;
30
+ emitOutput?: boolean;
40
31
  flags: Record<string, unknown>;
41
32
  kind?: string;
42
33
  name?: string;
34
+ printLine?: PrintLine;
43
35
  };
44
36
 
45
37
  type TemplatesExecutionInput = {
@@ -72,12 +64,164 @@ type SyncProjectContext = {
72
64
  scripts: Partial<Record<"sync" | "sync-rest" | "sync-types", string>>;
73
65
  };
74
66
 
67
+ const loadCliAddRuntime = () => import("@wp-typia/project-tools/cli-add");
68
+ const loadCliDoctorRuntime = () => import("@wp-typia/project-tools/cli-doctor");
69
+ const loadCliPromptRuntime = () => import("@wp-typia/project-tools/cli-prompt");
70
+ const loadCliScaffoldRuntime = () => import("@wp-typia/project-tools/cli-scaffold");
71
+ const loadCliTemplatesRuntime = () => import("@wp-typia/project-tools/cli-templates");
72
+ const loadMigrationsRuntime = () => import("@wp-typia/project-tools/migrations");
73
+
75
74
  function printBlock(lines: string[], printLine: PrintLine): void {
76
75
  for (const line of lines) {
77
76
  printLine(line);
78
77
  }
79
78
  }
80
79
 
80
+ export function printCompletionPayload(
81
+ payload: AlternateBufferCompletionPayload,
82
+ options: {
83
+ printLine?: PrintLine;
84
+ warnLine?: PrintLine;
85
+ } = {},
86
+ ): void {
87
+ const printLine = options.printLine ?? (console.log as PrintLine);
88
+ const warnLine = options.warnLine ?? printLine;
89
+
90
+ for (const line of payload.preambleLines ?? []) {
91
+ printLine(line);
92
+ }
93
+ for (const warning of payload.warningLines ?? []) {
94
+ warnLine(`⚠️ ${warning}`);
95
+ }
96
+
97
+ const hasDetails =
98
+ (payload.summaryLines?.length ?? 0) > 0 ||
99
+ (payload.nextSteps?.length ?? 0) > 0 ||
100
+ (payload.optionalLines?.length ?? 0) > 0 ||
101
+ Boolean(payload.optionalNote);
102
+ const hasLeadingContext =
103
+ (payload.preambleLines?.length ?? 0) > 0 ||
104
+ (payload.warningLines?.length ?? 0) > 0;
105
+
106
+ printLine(hasLeadingContext && hasDetails ? `\n${payload.title}` : payload.title);
107
+ for (const line of payload.summaryLines ?? []) {
108
+ printLine(line);
109
+ }
110
+ if ((payload.nextSteps?.length ?? 0) > 0) {
111
+ printLine("Next steps:");
112
+ for (const step of payload.nextSteps ?? []) {
113
+ printLine(` ${step}`);
114
+ }
115
+ }
116
+ if ((payload.optionalLines?.length ?? 0) > 0) {
117
+ printLine(`\n${payload.optionalTitle ?? "Optional:"}`);
118
+ for (const step of payload.optionalLines ?? []) {
119
+ printLine(` ${step}`);
120
+ }
121
+ }
122
+ if (payload.optionalNote) {
123
+ printLine(`Note: ${payload.optionalNote}`);
124
+ }
125
+ }
126
+
127
+ export function buildCreateCompletionPayload(flow: {
128
+ nextSteps: string[];
129
+ optionalOnboarding: {
130
+ note: string;
131
+ steps: string[];
132
+ };
133
+ projectDir: string;
134
+ result: {
135
+ selectedVariant?: string | null;
136
+ variables: {
137
+ title: string;
138
+ };
139
+ warnings: string[];
140
+ };
141
+ }): AlternateBufferCompletionPayload {
142
+ return {
143
+ nextSteps: flow.nextSteps,
144
+ optionalLines:
145
+ flow.optionalOnboarding.steps.length > 0 ? flow.optionalOnboarding.steps : undefined,
146
+ optionalNote:
147
+ flow.optionalOnboarding.steps.length > 0 ? flow.optionalOnboarding.note : undefined,
148
+ optionalTitle:
149
+ flow.optionalOnboarding.steps.length > 0 ? "Optional before first commit:" : undefined,
150
+ preambleLines: flow.result.selectedVariant
151
+ ? [`Template variant: ${flow.result.selectedVariant}`]
152
+ : undefined,
153
+ summaryLines: [`Project directory: ${flow.projectDir}`],
154
+ title: `✅ Created ${flow.result.variables.title} in ${flow.projectDir}`,
155
+ warningLines: flow.result.warnings,
156
+ };
157
+ }
158
+
159
+ export function buildMigrationCompletionPayload(options: {
160
+ command: string;
161
+ lines: string[];
162
+ }): AlternateBufferCompletionPayload {
163
+ const summaryLines = options.lines.filter((line) => line.trim().length > 0);
164
+
165
+ return {
166
+ summaryLines,
167
+ title: `✅ Completed wp-typia migrate ${options.command}`,
168
+ };
169
+ }
170
+
171
+ function buildAddCompletionPayload(options: {
172
+ kind: "binding-source" | "block" | "hooked-block" | "pattern" | "variation";
173
+ projectDir: string;
174
+ values: Record<string, string>;
175
+ }): AlternateBufferCompletionPayload {
176
+ switch (options.kind) {
177
+ case "variation":
178
+ return {
179
+ summaryLines: [
180
+ `Variation: ${options.values.variationSlug}`,
181
+ `Target block: ${options.values.blockSlug}`,
182
+ `Project directory: ${options.projectDir}`,
183
+ ],
184
+ title: "✅ Added workspace variation",
185
+ };
186
+ case "pattern":
187
+ return {
188
+ summaryLines: [
189
+ `Pattern: ${options.values.patternSlug}`,
190
+ `Project directory: ${options.projectDir}`,
191
+ ],
192
+ title: "✅ Added workspace pattern",
193
+ };
194
+ case "binding-source":
195
+ return {
196
+ summaryLines: [
197
+ `Binding source: ${options.values.bindingSourceSlug}`,
198
+ `Project directory: ${options.projectDir}`,
199
+ ],
200
+ title: "✅ Added binding source",
201
+ };
202
+ case "hooked-block":
203
+ return {
204
+ summaryLines: [
205
+ `Block: ${options.values.blockSlug}`,
206
+ `Anchor: ${options.values.anchorBlockName}`,
207
+ `Position: ${options.values.position}`,
208
+ `Project directory: ${options.projectDir}`,
209
+ ],
210
+ title: "✅ Added blockHooks metadata",
211
+ };
212
+ case "block":
213
+ default:
214
+ return {
215
+ summaryLines: [
216
+ `Blocks: ${options.values.blockSlugs}`,
217
+ `Template family: ${options.values.templateId}`,
218
+ `Project directory: ${options.projectDir}`,
219
+ ],
220
+ title: "✅ Added workspace block",
221
+ };
222
+ }
223
+ }
224
+
81
225
  function readOptionalStringFlag(
82
226
  flags: Record<string, unknown>,
83
227
  name: string,
@@ -268,10 +412,22 @@ const BOOLEAN_PROMPT_OPTIONS = [
268
412
  export async function executeCreateCommand({
269
413
  projectDir,
270
414
  cwd,
415
+ emitOutput = true,
271
416
  flags,
272
417
  interactive,
418
+ printLine = console.log as PrintLine,
273
419
  prompt,
274
- }: CreateExecutionInput): Promise<void> {
420
+ warnLine = console.warn as PrintLine,
421
+ }: CreateExecutionInput): Promise<AlternateBufferCompletionPayload> {
422
+ const [
423
+ { createReadlinePrompt },
424
+ { runScaffoldFlow },
425
+ { getTemplateSelectOptions },
426
+ ] = await Promise.all([
427
+ loadCliPromptRuntime(),
428
+ loadCliScaffoldRuntime(),
429
+ loadCliTemplatesRuntime(),
430
+ ]);
275
431
  const shouldPrompt =
276
432
  interactive ?? (!Boolean(flags.yes) && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
277
433
  const activePrompt = shouldPrompt ? (prompt ?? createReadlinePrompt()) : undefined;
@@ -331,25 +487,14 @@ export async function executeCreateCommand({
331
487
  yes: Boolean(flags.yes),
332
488
  });
333
489
 
334
- if (flow.result.selectedVariant) {
335
- console.log(`Template variant: ${flow.result.selectedVariant}`);
336
- }
337
- for (const warning of flow.result.warnings) {
338
- console.warn(`⚠️ ${warning}`);
339
- }
340
-
341
- console.log(`\n✅ Created ${flow.result.variables.title} in ${flow.projectDir}`);
342
- console.log("Next steps:");
343
- for (const step of flow.nextSteps) {
344
- console.log(` ${step}`);
345
- }
346
- if (flow.optionalOnboarding.steps.length > 0) {
347
- console.log("\nOptional before first commit:");
348
- for (const step of flow.optionalOnboarding.steps) {
349
- console.log(` ${step}`);
350
- }
351
- console.log(`Note: ${flow.optionalOnboarding.note}`);
490
+ const payload = buildCreateCompletionPayload(flow);
491
+ if (emitOutput) {
492
+ printCompletionPayload(payload, {
493
+ printLine,
494
+ warnLine,
495
+ });
352
496
  }
497
+ return payload;
353
498
  } finally {
354
499
  if (activePrompt && activePrompt !== prompt) {
355
500
  activePrompt.close();
@@ -359,15 +504,20 @@ export async function executeCreateCommand({
359
504
 
360
505
  export async function executeAddCommand({
361
506
  cwd,
507
+ emitOutput = true,
362
508
  flags,
363
509
  kind,
364
510
  name,
365
- }: AddExecutionInput): Promise<void> {
511
+ printLine = console.log as PrintLine,
512
+ }: AddExecutionInput): Promise<AlternateBufferCompletionPayload | void> {
366
513
  if (!kind) {
367
- console.log(formatAddHelpText());
514
+ const { formatAddHelpText } = await loadCliAddRuntime();
515
+ printLine(formatAddHelpText());
368
516
  return;
369
517
  }
370
518
 
519
+ const addRuntime = await loadCliAddRuntime();
520
+
371
521
  if (kind === "variation") {
372
522
  if (!name) {
373
523
  throw new Error(
@@ -380,13 +530,23 @@ export async function executeAddCommand({
380
530
  throw new Error("`wp-typia add variation` requires --block <block-slug>.");
381
531
  }
382
532
 
383
- const result = await runAddVariationCommand({
533
+ const result = await addRuntime.runAddVariationCommand({
384
534
  blockName: blockSlug,
385
535
  cwd,
386
536
  variationName: name,
387
537
  });
388
- console.log(`✅ Added variation ${result.variationSlug} to ${result.blockSlug} in ${result.projectDir}.`);
389
- return;
538
+ const payload = buildAddCompletionPayload({
539
+ kind: "variation",
540
+ projectDir: result.projectDir,
541
+ values: {
542
+ blockSlug: result.blockSlug,
543
+ variationSlug: result.variationSlug,
544
+ },
545
+ });
546
+ if (emitOutput) {
547
+ printCompletionPayload(payload, { printLine });
548
+ }
549
+ return payload;
390
550
  }
391
551
 
392
552
  if (kind === "pattern") {
@@ -396,12 +556,21 @@ export async function executeAddCommand({
396
556
  );
397
557
  }
398
558
 
399
- const result = await runAddPatternCommand({
559
+ const result = await addRuntime.runAddPatternCommand({
400
560
  cwd,
401
561
  patternName: name,
402
562
  });
403
- console.log(`✅ Added pattern ${result.patternSlug} in ${result.projectDir}.`);
404
- return;
563
+ const payload = buildAddCompletionPayload({
564
+ kind: "pattern",
565
+ projectDir: result.projectDir,
566
+ values: {
567
+ patternSlug: result.patternSlug,
568
+ },
569
+ });
570
+ if (emitOutput) {
571
+ printCompletionPayload(payload, { printLine });
572
+ }
573
+ return payload;
405
574
  }
406
575
 
407
576
  if (kind === "binding-source") {
@@ -411,12 +580,21 @@ export async function executeAddCommand({
411
580
  );
412
581
  }
413
582
 
414
- const result = await runAddBindingSourceCommand({
583
+ const result = await addRuntime.runAddBindingSourceCommand({
415
584
  bindingSourceName: name,
416
585
  cwd,
417
586
  });
418
- console.log(`✅ Added binding source ${result.bindingSourceSlug} in ${result.projectDir}.`);
419
- return;
587
+ const payload = buildAddCompletionPayload({
588
+ kind: "binding-source",
589
+ projectDir: result.projectDir,
590
+ values: {
591
+ bindingSourceSlug: result.bindingSourceSlug,
592
+ },
593
+ });
594
+ if (emitOutput) {
595
+ printCompletionPayload(payload, { printLine });
596
+ }
597
+ return payload;
420
598
  }
421
599
 
422
600
  if (kind === "hooked-block") {
@@ -438,16 +616,25 @@ export async function executeAddCommand({
438
616
  );
439
617
  }
440
618
 
441
- const result = await runAddHookedBlockCommand({
619
+ const result = await addRuntime.runAddHookedBlockCommand({
442
620
  anchorBlockName,
443
621
  blockName: name,
444
622
  cwd,
445
623
  position,
446
624
  });
447
- console.log(
448
- `✅ Added blockHooks metadata for ${result.blockSlug} relative to ${result.anchorBlockName} (${result.position}) in ${result.projectDir}.`,
449
- );
450
- return;
625
+ const payload = buildAddCompletionPayload({
626
+ kind: "hooked-block",
627
+ projectDir: result.projectDir,
628
+ values: {
629
+ anchorBlockName: result.anchorBlockName,
630
+ blockSlug: result.blockSlug,
631
+ position: result.position,
632
+ },
633
+ });
634
+ if (emitOutput) {
635
+ printCompletionPayload(payload, { printLine });
636
+ }
637
+ return payload;
451
638
  }
452
639
 
453
640
  if (kind !== "block") {
@@ -468,7 +655,7 @@ export async function executeAddCommand({
468
655
  );
469
656
  }
470
657
 
471
- const result = await runAddBlockCommand({
658
+ const result = await addRuntime.runAddBlockCommand({
472
659
  blockName: name,
473
660
  cwd,
474
661
  dataStorageMode: readOptionalStringFlag(flags, "data-storage"),
@@ -480,9 +667,18 @@ export async function executeAddCommand({
480
667
  | "compound",
481
668
  });
482
669
 
483
- console.log(
484
- `✅ Added ${result.blockSlugs.join(", ")} to ${result.projectDir} using the ${result.templateId} family.`,
485
- );
670
+ const payload = buildAddCompletionPayload({
671
+ kind: "block",
672
+ projectDir: result.projectDir,
673
+ values: {
674
+ blockSlugs: result.blockSlugs.join(", "),
675
+ templateId: result.templateId,
676
+ },
677
+ });
678
+ if (emitOutput) {
679
+ printCompletionPayload(payload, { printLine });
680
+ }
681
+ return payload;
486
682
  }
487
683
 
488
684
  export async function executeTemplatesCommand(
@@ -524,9 +720,20 @@ export async function executeTemplatesCommand(
524
720
  }
525
721
 
526
722
  export async function executeDoctorCommand(cwd: string): Promise<void> {
723
+ const { runDoctor } = await loadCliDoctorRuntime();
527
724
  await runDoctor(cwd);
528
725
  }
529
726
 
727
+ export async function loadAddWorkspaceBlockOptions(cwd: string) {
728
+ const workspace = tryResolveWorkspaceProject(cwd);
729
+ if (!workspace) {
730
+ return [];
731
+ }
732
+
733
+ const { getWorkspaceBlockSelectOptions } = await loadCliAddRuntime();
734
+ return getWorkspaceBlockSelectOptions(workspace.projectDir);
735
+ }
736
+
530
737
  export async function executeSyncCommand({
531
738
  check = false,
532
739
  cwd,
@@ -552,7 +759,9 @@ export async function executeMigrateCommand({
552
759
  flags,
553
760
  prompt,
554
761
  renderLine,
555
- }: MigrateExecutionInput): Promise<void> {
762
+ }: MigrateExecutionInput): Promise<AlternateBufferCompletionPayload | void> {
763
+ const { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand } =
764
+ await loadMigrationsRuntime();
556
765
  if (!command) {
557
766
  console.log(formatMigrationHelpText());
558
767
  return;
@@ -581,19 +790,31 @@ export async function executeMigrateCommand({
581
790
  pushFlag(argv, "seed", readOptionalLooseStringFlag(flags, "seed"));
582
791
 
583
792
  const parsed = parseMigrationArgs(argv);
584
- await runMigrationCommand(parsed, cwd, {
793
+ const lines: string[] | null = renderLine ? [] : null;
794
+ const captureLine = (line: string) => {
795
+ lines?.push(line);
796
+ if (renderLine) {
797
+ renderLine(line);
798
+ return;
799
+ }
800
+ console.log(line);
801
+ };
802
+ const result = await runMigrationCommand(parsed, cwd, {
585
803
  prompt,
586
- renderLine,
804
+ renderLine: captureLine,
587
805
  });
588
- }
589
-
590
- export function getAddWorkspaceBlockOptions(cwd: string) {
591
- const workspace = tryResolveWorkspaceProject(cwd);
592
- if (!workspace) {
593
- return [];
806
+ if (renderLine) {
807
+ return result && typeof result === "object" && "cancelled" in result && result.cancelled === true
808
+ ? undefined
809
+ : buildMigrationCompletionPayload({
810
+ command: parsed.command ?? "plan",
811
+ lines: lines ?? [],
812
+ });
594
813
  }
595
814
 
596
- return getWorkspaceBlockSelectOptions(workspace.projectDir);
815
+ if (result && typeof result === "object" && "cancelled" in result && result.cancelled === true) {
816
+ return;
817
+ }
597
818
  }
598
819
 
599
- export { formatAddHelpText, formatMigrationHelpText, listTemplates };
820
+ export { listTemplates };
@@ -1,4 +1,4 @@
1
- import { createElement, useMemo } from "react";
1
+ import { createElement, useEffect, useMemo, useState } from "react";
2
2
 
3
3
  import {
4
4
  Form,
@@ -6,9 +6,9 @@ import {
6
6
  useFormContext,
7
7
  useTerminalDimensions,
8
8
  } from "@bunli/tui";
9
- import { HOOKED_BLOCK_POSITION_IDS } from "@wp-typia/project-tools";
9
+ import { HOOKED_BLOCK_POSITION_IDS } from "@wp-typia/project-tools/hooked-blocks";
10
10
 
11
- import { executeAddCommand } from "../runtime-bridge";
11
+ import { executeAddCommand, loadAddWorkspaceBlockOptions } from "../runtime-bridge";
12
12
  import { useAlternateBufferLifecycle } from "./alternate-buffer-lifecycle";
13
13
  import {
14
14
  type AddFlowValues,
@@ -20,7 +20,8 @@ import {
20
20
  sanitizeAddSubmitValues,
21
21
  } from "./add-flow-model";
22
22
  import {
23
- FirstPartyScrollBox,
23
+ FirstPartyCompletionViewport,
24
+ FirstPartyFormViewport,
24
25
  FirstPartySelectField,
25
26
  FirstPartyTextField,
26
27
  } from "./first-party-form";
@@ -104,11 +105,12 @@ const HOOKED_BLOCK_POSITION_DESCRIPTIONS: Record<
104
105
  type AddFlowProps = {
105
106
  cwd: string;
106
107
  initialValues: Partial<AddFlowValues>;
107
- workspaceBlockOptions: Array<{
108
- description: string;
109
- name: string;
110
- value: string;
111
- }>;
108
+ };
109
+
110
+ type WorkspaceBlockOption = {
111
+ description: string;
112
+ name: string;
113
+ value: string;
112
114
  };
113
115
 
114
116
  type AddSelectFieldName = {
@@ -134,9 +136,9 @@ function getAddNameLabel(kind?: string): string {
134
136
  function AddFlowFields({
135
137
  workspaceBlockOptions,
136
138
  }: {
137
- workspaceBlockOptions: AddFlowProps["workspaceBlockOptions"];
139
+ workspaceBlockOptions: WorkspaceBlockOption[];
138
140
  }) {
139
- const { activeFieldName, values } = useFormContext();
141
+ const { activeFieldName, isSubmitting, values } = useFormContext();
140
142
  const { height: terminalHeight = 24 } = useTerminalDimensions();
141
143
  const addValues = values as Partial<AddFlowValues>;
142
144
  const kind = addValues.kind ?? "block";
@@ -159,8 +161,14 @@ function AddFlowFields({
159
161
  const variationBlockUsesSelect = kind === "variation" && workspaceBlockOptions.length > 0;
160
162
 
161
163
  return createElement(
162
- FirstPartyScrollBox,
163
- { scrollTop, viewportHeight },
164
+ FirstPartyFormViewport,
165
+ {
166
+ isSubmitting,
167
+ scrollTop,
168
+ submittingDescription: "Applying your workspace changes...",
169
+ submittingTitle: "Updating workspace...",
170
+ viewportHeight,
171
+ },
164
172
  [
165
173
  createElement(FirstPartySelectField, {
166
174
  ...getWrappedFieldNeighbors(orderedVisibleFields, "kind"),
@@ -255,8 +263,41 @@ function AddFlowFields({
255
263
  );
256
264
  }
257
265
 
258
- export function AddFlow({ cwd, initialValues, workspaceBlockOptions }: AddFlowProps) {
259
- const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia add failed");
266
+ export function AddFlow({ cwd, initialValues }: AddFlowProps) {
267
+ const { completion, handleCancel, handleFailure, handleSubmit, status } =
268
+ useAlternateBufferLifecycle(
269
+ "wp-typia add failed",
270
+ );
271
+ const { height: terminalHeight = 24 } = useTerminalDimensions();
272
+ const [workspaceBlockOptions, setWorkspaceBlockOptions] = useState<WorkspaceBlockOption[]>([]);
273
+
274
+ useEffect(() => {
275
+ let disposed = false;
276
+ setWorkspaceBlockOptions([]);
277
+
278
+ void loadAddWorkspaceBlockOptions(cwd)
279
+ .then((options) => {
280
+ if (!disposed) {
281
+ setWorkspaceBlockOptions(options);
282
+ }
283
+ })
284
+ .catch((error) => {
285
+ if (!disposed) {
286
+ handleFailure(error);
287
+ }
288
+ });
289
+
290
+ return () => {
291
+ disposed = true;
292
+ };
293
+ }, [cwd, handleFailure]);
294
+
295
+ if (status === "completed" && completion) {
296
+ return createElement(FirstPartyCompletionViewport, {
297
+ completion,
298
+ viewportHeight: getAddViewportHeight(terminalHeight),
299
+ });
300
+ }
260
301
 
261
302
  return (
262
303
  <Form
@@ -265,8 +306,9 @@ export function AddFlow({ cwd, initialValues, workspaceBlockOptions }: AddFlowPr
265
306
  onSubmit={async (values) =>
266
307
  handleSubmit(async () => {
267
308
  const flags = sanitizeAddSubmitValues(values);
268
- await executeAddCommand({
309
+ return executeAddCommand({
269
310
  cwd,
311
+ emitOutput: false,
270
312
  flags,
271
313
  kind: values.kind,
272
314
  name: typeof flags.name === "string" ? flags.name : undefined,
@@ -1,13 +1,25 @@
1
- import { useCallback } from "react";
1
+ import { useCallback, useState } from "react";
2
2
 
3
3
  import { useRuntime } from "@bunli/runtime/app";
4
4
  import { useKeyboard } from "@bunli/tui";
5
5
 
6
6
  type AlternateBufferKeyEvent = {
7
7
  ctrl?: boolean;
8
+ sequence?: string;
8
9
  name?: string;
9
10
  };
10
11
 
12
+ export type AlternateBufferCompletionPayload = {
13
+ title: string;
14
+ preambleLines?: string[];
15
+ summaryLines?: string[];
16
+ nextSteps?: string[];
17
+ optionalTitle?: string;
18
+ optionalLines?: string[];
19
+ optionalNote?: string;
20
+ warningLines?: string[];
21
+ };
22
+
11
23
  type AlternateBufferFailureOptions = {
12
24
  context: string;
13
25
  error: unknown;
@@ -16,12 +28,16 @@ type AlternateBufferFailureOptions = {
16
28
  };
17
29
 
18
30
  type RunAlternateBufferActionOptions = {
19
- action: () => Promise<void>;
31
+ action: () => Promise<unknown>;
20
32
  context: string;
21
33
  exit: () => void;
34
+ exitOnSuccess?: boolean;
22
35
  log?: (message: string) => void;
36
+ onSuccess?: (result: unknown) => void;
23
37
  };
24
38
 
39
+ type AlternateBufferLifecycleStatus = "editing" | "submitting" | "completed";
40
+
25
41
  export function describeAlternateBufferFailure(context: string, error: unknown): string {
26
42
  const message = error instanceof Error ? error.message : String(error);
27
43
  return `${context}: ${message}`;
@@ -31,6 +47,10 @@ export function isAlternateBufferExitKey(key: AlternateBufferKeyEvent): boolean
31
47
  return key.name === "q" || (key.ctrl === true && key.name === "c");
32
48
  }
33
49
 
50
+ export function isAlternateBufferCompletionKey(key: AlternateBufferKeyEvent): boolean {
51
+ return key.name === "enter" || key.sequence === "\r" || key.sequence === "\n";
52
+ }
53
+
34
54
  export function reportAlternateBufferFailure({
35
55
  context,
36
56
  error,
@@ -46,11 +66,16 @@ export async function runAlternateBufferAction({
46
66
  action,
47
67
  context,
48
68
  exit,
69
+ exitOnSuccess = true,
49
70
  log = console.error,
71
+ onSuccess,
50
72
  }: RunAlternateBufferActionOptions): Promise<void> {
51
73
  try {
52
- await action();
53
- exit();
74
+ const result = await action();
75
+ onSuccess?.(result);
76
+ if (exitOnSuccess) {
77
+ exit();
78
+ }
54
79
  } catch (error) {
55
80
  reportAlternateBufferFailure({ context, error, exit, log });
56
81
  }
@@ -98,32 +123,75 @@ export function useAlternateBufferExitKeys(options: {
98
123
  });
99
124
  }
100
125
 
126
+ export function useAlternateBufferCompletionKeys(options: {
127
+ enabled?: boolean;
128
+ exit?: () => void;
129
+ } = {}): void {
130
+ const runtime = useRuntime();
131
+ const exit = options.exit ?? (() => runtime.exit());
132
+ const enabled = options.enabled ?? false;
133
+
134
+ useKeyboard((key: AlternateBufferKeyEvent) => {
135
+ if (!enabled) {
136
+ return;
137
+ }
138
+
139
+ if (isAlternateBufferCompletionKey(key) || isAlternateBufferExitKey(key)) {
140
+ exit();
141
+ }
142
+ });
143
+ }
144
+
145
+ function isAlternateBufferCompletionPayload(
146
+ value: unknown,
147
+ ): value is AlternateBufferCompletionPayload {
148
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
149
+ return false;
150
+ }
151
+
152
+ const candidate = value as { title?: unknown };
153
+ return typeof candidate.title === "string" && candidate.title.trim().length > 0;
154
+ }
155
+
101
156
  export function useAlternateBufferLifecycle(
102
157
  context: string,
103
158
  options: {
104
159
  enableExitKeys?: boolean;
105
160
  } = {},
106
161
  ): {
162
+ completion: AlternateBufferCompletionPayload | null;
107
163
  handleCancel: () => void;
108
164
  handleFailure: (error: unknown) => void;
109
- handleSubmit: (action: () => Promise<void>) => Promise<void>;
165
+ handleSubmit: (action: () => Promise<AlternateBufferCompletionPayload | void>) => Promise<void>;
166
+ status: AlternateBufferLifecycleStatus;
110
167
  } {
111
168
  const runtime = useRuntime();
169
+ const [completion, setCompletion] = useState<AlternateBufferCompletionPayload | null>(null);
170
+ const [status, setStatus] = useState<AlternateBufferLifecycleStatus>("editing");
112
171
  const exit = useCallback(() => {
113
172
  runtime.exit();
114
173
  }, [runtime]);
115
174
 
116
175
  useAlternateBufferExitKeys({
117
- enabled: options.enableExitKeys ?? true,
176
+ enabled: (options.enableExitKeys ?? true) && status !== "completed",
177
+ exit,
178
+ });
179
+
180
+ useAlternateBufferCompletionKeys({
181
+ enabled: status === "completed",
118
182
  exit,
119
183
  });
120
184
 
121
185
  const handleCancel = useCallback(() => {
186
+ setCompletion(null);
187
+ setStatus("editing");
122
188
  exit();
123
189
  }, [exit]);
124
190
 
125
191
  const handleFailure = useCallback(
126
192
  (error: unknown) => {
193
+ setCompletion(null);
194
+ setStatus("editing");
127
195
  reportAlternateBufferFailure({
128
196
  context,
129
197
  error,
@@ -134,19 +202,37 @@ export function useAlternateBufferLifecycle(
134
202
  );
135
203
 
136
204
  const handleSubmit = useCallback(
137
- async (action: () => Promise<void>) => {
138
- await runAlternateBufferAction({
139
- action,
140
- context,
141
- exit,
142
- });
205
+ async (action: () => Promise<AlternateBufferCompletionPayload | void>) => {
206
+ setCompletion(null);
207
+ setStatus("submitting");
208
+
209
+ try {
210
+ const result = await action();
211
+ if (isAlternateBufferCompletionPayload(result)) {
212
+ setCompletion(result);
213
+ setStatus("completed");
214
+ return;
215
+ }
216
+
217
+ exit();
218
+ } catch (error) {
219
+ setCompletion(null);
220
+ setStatus("editing");
221
+ reportAlternateBufferFailure({
222
+ context,
223
+ error,
224
+ exit,
225
+ });
226
+ }
143
227
  },
144
228
  [context, exit],
145
229
  );
146
230
 
147
231
  return {
232
+ completion,
148
233
  handleCancel,
149
234
  handleFailure,
150
235
  handleSubmit,
236
+ status,
151
237
  };
152
238
  }
@@ -21,7 +21,8 @@ import {
21
21
  } from "./create-flow-model";
22
22
  import {
23
23
  FirstPartyCheckboxField,
24
- FirstPartyScrollBox,
24
+ FirstPartyCompletionViewport,
25
+ FirstPartyFormViewport,
25
26
  FirstPartySelectField,
26
27
  FirstPartyTextField,
27
28
  } from "./first-party-form";
@@ -70,7 +71,7 @@ type CreateSelectFieldName = {
70
71
  }[keyof CreateFlowValues];
71
72
 
72
73
  function CreateFlowFields() {
73
- const { activeFieldName, values } = useFormContext();
74
+ const { activeFieldName, isSubmitting, values } = useFormContext();
74
75
  const { height: terminalHeight = 24 } = useTerminalDimensions();
75
76
  const createValues = values as Partial<CreateFlowValues>;
76
77
  const template = createValues.template;
@@ -88,8 +89,14 @@ function CreateFlowFields() {
88
89
  );
89
90
 
90
91
  return createElement(
91
- FirstPartyScrollBox,
92
- { scrollTop, viewportHeight },
92
+ FirstPartyFormViewport,
93
+ {
94
+ isSubmitting,
95
+ scrollTop,
96
+ submittingDescription: "Preparing your wp-typia project files...",
97
+ submittingTitle: "Creating project...",
98
+ viewportHeight,
99
+ },
93
100
  [
94
101
  createElement(FirstPartyTextField, {
95
102
  ...getWrappedFieldNeighbors(visibleFields, "project-dir"),
@@ -161,7 +168,10 @@ function CreateFlowFields() {
161
168
  }
162
169
 
163
170
  export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
164
- const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia create failed");
171
+ const { completion, handleCancel, handleSubmit, status } = useAlternateBufferLifecycle(
172
+ "wp-typia create failed",
173
+ );
174
+ const { height: terminalHeight = 24 } = useTerminalDimensions();
165
175
  const defaultPrompt = {
166
176
  close() {},
167
177
  select<T extends string>(_message: string, options: Array<{ value: T }>, defaultValue = 1) {
@@ -173,6 +183,13 @@ export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
173
183
  },
174
184
  };
175
185
 
186
+ if (status === "completed" && completion) {
187
+ return createElement(FirstPartyCompletionViewport, {
188
+ completion,
189
+ viewportHeight: getCreateViewportHeight(terminalHeight),
190
+ });
191
+ }
192
+
176
193
  return (
177
194
  <Form
178
195
  initialValues={initialValues}
@@ -180,8 +197,9 @@ export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
180
197
  onSubmit={async (values) =>
181
198
  handleSubmit(async () => {
182
199
  const flags = sanitizeCreateSubmitValues(values);
183
- await executeCreateCommand({
200
+ return executeCreateCommand({
184
201
  cwd,
202
+ emitOutput: false,
185
203
  flags,
186
204
  interactive: true,
187
205
  projectDir: values["project-dir"],
@@ -10,6 +10,7 @@ import {
10
10
 
11
11
  import { useScopedKeyboard } from "@bunli/runtime/app";
12
12
  import {
13
+ Spinner,
13
14
  type SelectOption,
14
15
  createKeyMatcher,
15
16
  useFormContext,
@@ -23,6 +24,7 @@ import {
23
24
  FIRST_PARTY_SELECT_FIELD_CONTROL_HEIGHT,
24
25
  FIRST_PARTY_SELECT_FIELD_LABEL_GAP,
25
26
  } from "./first-party-form-model";
27
+ import type { AlternateBufferCompletionPayload } from "./alternate-buffer-lifecycle";
26
28
 
27
29
  const checkboxKeymap = createKeyMatcher({
28
30
  toggle: ["space", "enter"],
@@ -96,6 +98,30 @@ function isCheckboxToggleKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
96
98
  );
97
99
  }
98
100
 
101
+ function isCompletionLineDownKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
102
+ return key.name === "down" || key.sequence === "\x1b[B";
103
+ }
104
+
105
+ function isCompletionLineUpKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
106
+ return key.name === "up" || key.sequence === "\x1b[A";
107
+ }
108
+
109
+ function isCompletionPageDownKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
110
+ return key.name === "pagedown" || key.sequence === "\x1b[6~" || key.sequence === " ";
111
+ }
112
+
113
+ function isCompletionPageUpKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
114
+ return key.name === "pageup" || key.sequence === "\x1b[5~";
115
+ }
116
+
117
+ function isCompletionHomeKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
118
+ return key.name === "home" || key.sequence === "\x1b[H" || key.sequence === "\x1bOH";
119
+ }
120
+
121
+ function isCompletionEndKey(key: Parameters<typeof checkboxKeymap.match>[1]) {
122
+ return key.name === "end" || key.sequence === "\x1b[F" || key.sequence === "\x1bOF";
123
+ }
124
+
99
125
  function useFirstPartyFieldNavigation(options: {
100
126
  focused: boolean;
101
127
  keyboardScopeId: string;
@@ -458,3 +484,237 @@ export function FirstPartyScrollBox({
458
484
  ),
459
485
  );
460
486
  }
487
+
488
+ export function FirstPartySubmittingSurface({
489
+ description = "Please wait while wp-typia finishes this command.",
490
+ title = "Submitting...",
491
+ viewportHeight,
492
+ }: {
493
+ description?: string;
494
+ title?: string;
495
+ viewportHeight: number;
496
+ }) {
497
+ const { tokens } = useTuiTheme();
498
+
499
+ return createElement(
500
+ "box",
501
+ {
502
+ border: true,
503
+ height: viewportHeight,
504
+ width: "100%",
505
+ "data-form-surface": "submitting",
506
+ style: {
507
+ alignItems: "center",
508
+ borderColor: tokens.borderMuted,
509
+ flexDirection: "column",
510
+ gap: 1,
511
+ justifyContent: "center",
512
+ },
513
+ },
514
+ createElement(Spinner, {
515
+ title,
516
+ variant: "dot",
517
+ }),
518
+ createElement("text", {
519
+ content: description,
520
+ fg: tokens.textMuted,
521
+ }),
522
+ );
523
+ }
524
+
525
+ export function FirstPartyFormViewport({
526
+ children,
527
+ isSubmitting = false,
528
+ scrollTop,
529
+ submittingDescription,
530
+ submittingTitle,
531
+ viewportHeight,
532
+ }: {
533
+ children?: ReactNode;
534
+ isSubmitting?: boolean;
535
+ scrollTop: number;
536
+ submittingDescription?: string;
537
+ submittingTitle?: string;
538
+ viewportHeight: number;
539
+ }) {
540
+ if (isSubmitting) {
541
+ return createElement(FirstPartySubmittingSurface, {
542
+ description: submittingDescription,
543
+ title: submittingTitle,
544
+ viewportHeight,
545
+ });
546
+ }
547
+
548
+ return createElement(FirstPartyScrollBox, {
549
+ scrollTop,
550
+ viewportHeight,
551
+ children,
552
+ });
553
+ }
554
+
555
+ export function FirstPartyCompletionViewport({
556
+ completion,
557
+ viewportHeight,
558
+ }: {
559
+ completion: AlternateBufferCompletionPayload;
560
+ viewportHeight: number;
561
+ }) {
562
+ const { tokens } = useTuiTheme();
563
+ const reactScopeId = useId();
564
+ const keyboardScopeId = `first-party-completion:${reactScopeId}`;
565
+ const bodyHeight = Math.max(4, viewportHeight - 3);
566
+ const bodyRef = useRef<{ scrollTop: number } | null>(null);
567
+
568
+ const setCompletionScrollTop = useCallback((nextScrollTop: number) => {
569
+ if (!bodyRef.current) {
570
+ return false;
571
+ }
572
+
573
+ bodyRef.current.scrollTop = Math.max(0, nextScrollTop);
574
+ return true;
575
+ }, []);
576
+
577
+ const adjustCompletionScrollTop = useCallback(
578
+ (delta: number) => {
579
+ if (!bodyRef.current) {
580
+ return false;
581
+ }
582
+
583
+ bodyRef.current.scrollTop = Math.max(0, bodyRef.current.scrollTop + delta);
584
+ return true;
585
+ },
586
+ [],
587
+ );
588
+
589
+ useScopedKeyboard(
590
+ keyboardScopeId,
591
+ (key) => {
592
+ if (isCompletionLineDownKey(key)) {
593
+ return adjustCompletionScrollTop(1);
594
+ }
595
+ if (isCompletionLineUpKey(key)) {
596
+ return adjustCompletionScrollTop(-1);
597
+ }
598
+ if (isCompletionPageDownKey(key)) {
599
+ return adjustCompletionScrollTop(Math.max(1, bodyHeight - 1));
600
+ }
601
+ if (isCompletionPageUpKey(key)) {
602
+ return adjustCompletionScrollTop(-Math.max(1, bodyHeight - 1));
603
+ }
604
+ if (isCompletionHomeKey(key)) {
605
+ return setCompletionScrollTop(0);
606
+ }
607
+ if (isCompletionEndKey(key)) {
608
+ return setCompletionScrollTop(Number.MAX_SAFE_INTEGER);
609
+ }
610
+
611
+ return false;
612
+ },
613
+ { active: true },
614
+ );
615
+
616
+ return createElement(
617
+ "box",
618
+ {
619
+ border: true,
620
+ height: viewportHeight,
621
+ width: "100%",
622
+ "data-form-surface": "completed",
623
+ style: {
624
+ borderColor: tokens.borderMuted,
625
+ flexDirection: "column",
626
+ },
627
+ },
628
+ createElement(
629
+ "scrollbox",
630
+ {
631
+ ref: bodyRef,
632
+ height: bodyHeight,
633
+ scrollY: true,
634
+ scrollbarOptions: {
635
+ visible: true,
636
+ trackOptions: {
637
+ backgroundColor: tokens.backgroundMuted,
638
+ foregroundColor: tokens.borderMuted,
639
+ },
640
+ },
641
+ viewportOptions: { width: "100%" },
642
+ contentOptions: { width: "100%" },
643
+ },
644
+ createElement(
645
+ "box",
646
+ {
647
+ width: "100%",
648
+ style: {
649
+ flexDirection: "column",
650
+ gap: 1,
651
+ },
652
+ },
653
+ createElement("text", {
654
+ content: completion.title,
655
+ fg: tokens.accent,
656
+ }),
657
+ ...(completion.preambleLines ?? []).map((line, index) =>
658
+ createElement("text", {
659
+ content: line,
660
+ fg: tokens.textMuted,
661
+ key: `preamble:${index}`,
662
+ }),
663
+ ),
664
+ ...(completion.warningLines ?? []).map((line, index) =>
665
+ createElement("text", {
666
+ content: `⚠️ ${line}`,
667
+ fg: tokens.textWarning,
668
+ key: `warning:${index}`,
669
+ }),
670
+ ),
671
+ ...(completion.summaryLines ?? []).map((line, index) =>
672
+ createElement("text", {
673
+ content: line,
674
+ fg: tokens.textPrimary,
675
+ key: `summary:${index}`,
676
+ }),
677
+ ),
678
+ (completion.nextSteps?.length ?? 0) > 0
679
+ ? createElement("text", {
680
+ content: "Next steps:",
681
+ fg: tokens.textPrimary,
682
+ key: "next-steps:title",
683
+ })
684
+ : null,
685
+ ...(completion.nextSteps ?? []).map((line, index) =>
686
+ createElement("text", {
687
+ content: ` ${line}`,
688
+ fg: tokens.textPrimary,
689
+ key: `next-step:${index}`,
690
+ }),
691
+ ),
692
+ (completion.optionalLines?.length ?? 0) > 0
693
+ ? createElement("text", {
694
+ content: completion.optionalTitle ?? "Optional:",
695
+ fg: tokens.textPrimary,
696
+ key: "optional:title",
697
+ })
698
+ : null,
699
+ ...(completion.optionalLines ?? []).map((line, index) =>
700
+ createElement("text", {
701
+ content: ` ${line}`,
702
+ fg: tokens.textMuted,
703
+ key: `optional:${index}`,
704
+ }),
705
+ ),
706
+ completion.optionalNote
707
+ ? createElement("text", {
708
+ content: `Note: ${completion.optionalNote}`,
709
+ fg: tokens.textMuted,
710
+ key: "optional:note",
711
+ })
712
+ : null,
713
+ ),
714
+ ),
715
+ createElement("text", {
716
+ content: "PgUp/PgDn | ↑/↓ | Home/End | Enter: close | q: exit | Ctrl+C: quit",
717
+ fg: tokens.textMuted,
718
+ }),
719
+ );
720
+ }
@@ -19,7 +19,8 @@ import {
19
19
  } from "./migrate-flow-model";
20
20
  import {
21
21
  FirstPartyCheckboxField,
22
- FirstPartyScrollBox,
22
+ FirstPartyCompletionViewport,
23
+ FirstPartyFormViewport,
23
24
  FirstPartySelectField,
24
25
  FirstPartyTextField,
25
26
  } from "./first-party-form";
@@ -76,7 +77,7 @@ type MigrateCheckboxFieldName = {
76
77
  }[keyof MigrateFlowValues];
77
78
 
78
79
  function MigrateFlowFields() {
79
- const { activeFieldName, values } = useFormContext();
80
+ const { activeFieldName, isSubmitting, values } = useFormContext();
80
81
  const { height: terminalHeight = 24 } = useTerminalDimensions();
81
82
  const migrateValues = values as Partial<MigrateFlowValues>;
82
83
  const command = migrateValues.command ?? "plan";
@@ -98,8 +99,14 @@ function MigrateFlowFields() {
98
99
  );
99
100
 
100
101
  return createElement(
101
- FirstPartyScrollBox,
102
- { scrollTop, viewportHeight },
102
+ FirstPartyFormViewport,
103
+ {
104
+ isSubmitting,
105
+ scrollTop,
106
+ submittingDescription: "Running the selected migration workflow...",
107
+ submittingTitle: "Running migration...",
108
+ viewportHeight,
109
+ },
103
110
  [
104
111
  createElement(FirstPartySelectField, {
105
112
  ...getWrappedFieldNeighbors(orderedVisibleFields, "command"),
@@ -180,7 +187,17 @@ function MigrateFlowFields() {
180
187
  }
181
188
 
182
189
  export function MigrateFlow({ cwd, initialValues }: MigrateFlowProps) {
183
- const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia migrate failed");
190
+ const { completion, handleCancel, handleSubmit, status } = useAlternateBufferLifecycle(
191
+ "wp-typia migrate failed",
192
+ );
193
+ const { height: terminalHeight = 24 } = useTerminalDimensions();
194
+
195
+ if (status === "completed" && completion) {
196
+ return createElement(FirstPartyCompletionViewport, {
197
+ completion,
198
+ viewportHeight: getMigrateViewportHeight(terminalHeight),
199
+ });
200
+ }
184
201
 
185
202
  return (
186
203
  <Form
@@ -189,10 +206,11 @@ export function MigrateFlow({ cwd, initialValues }: MigrateFlowProps) {
189
206
  onSubmit={async (values) =>
190
207
  handleSubmit(async () => {
191
208
  const flags = sanitizeMigrateSubmitValues(values);
192
- await executeMigrateCommand({
209
+ return executeMigrateCommand({
193
210
  command: values.command,
194
211
  cwd,
195
212
  flags,
213
+ renderLine: () => undefined,
196
214
  });
197
215
  })
198
216
  }