wp-typia 0.16.3 → 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.3",
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,7 +65,7 @@
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.2",
68
+ "@wp-typia/project-tools": "0.16.4",
69
69
  "better-result": "^2.7.0",
70
70
  "react": "^19.2.5",
71
71
  "react-dom": "^19.2.5",
@@ -12,20 +12,26 @@ import {
12
12
  import { formatRunScript } from "@wp-typia/project-tools/package-managers";
13
13
  import { tryResolveWorkspaceProject } from "@wp-typia/project-tools/workspace-project";
14
14
  import type { ReadlinePrompt } from "@wp-typia/project-tools/cli-prompt";
15
+ import type { AlternateBufferCompletionPayload } from "./ui/alternate-buffer-lifecycle";
15
16
 
16
17
  type CreateExecutionInput = {
17
18
  projectDir: string;
18
19
  cwd: string;
20
+ emitOutput?: boolean;
19
21
  flags: Record<string, unknown>;
20
22
  interactive?: boolean;
23
+ printLine?: PrintLine;
21
24
  prompt?: ReadlinePrompt;
25
+ warnLine?: PrintLine;
22
26
  };
23
27
 
24
28
  type AddExecutionInput = {
25
29
  cwd: string;
30
+ emitOutput?: boolean;
26
31
  flags: Record<string, unknown>;
27
32
  kind?: string;
28
33
  name?: string;
34
+ printLine?: PrintLine;
29
35
  };
30
36
 
31
37
  type TemplatesExecutionInput = {
@@ -71,6 +77,151 @@ function printBlock(lines: string[], printLine: PrintLine): void {
71
77
  }
72
78
  }
73
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
+
74
225
  function readOptionalStringFlag(
75
226
  flags: Record<string, unknown>,
76
227
  name: string,
@@ -261,10 +412,13 @@ const BOOLEAN_PROMPT_OPTIONS = [
261
412
  export async function executeCreateCommand({
262
413
  projectDir,
263
414
  cwd,
415
+ emitOutput = true,
264
416
  flags,
265
417
  interactive,
418
+ printLine = console.log as PrintLine,
266
419
  prompt,
267
- }: CreateExecutionInput): Promise<void> {
420
+ warnLine = console.warn as PrintLine,
421
+ }: CreateExecutionInput): Promise<AlternateBufferCompletionPayload> {
268
422
  const [
269
423
  { createReadlinePrompt },
270
424
  { runScaffoldFlow },
@@ -333,25 +487,14 @@ export async function executeCreateCommand({
333
487
  yes: Boolean(flags.yes),
334
488
  });
335
489
 
336
- if (flow.result.selectedVariant) {
337
- console.log(`Template variant: ${flow.result.selectedVariant}`);
338
- }
339
- for (const warning of flow.result.warnings) {
340
- console.warn(`⚠️ ${warning}`);
341
- }
342
-
343
- console.log(`\n✅ Created ${flow.result.variables.title} in ${flow.projectDir}`);
344
- console.log("Next steps:");
345
- for (const step of flow.nextSteps) {
346
- console.log(` ${step}`);
347
- }
348
- if (flow.optionalOnboarding.steps.length > 0) {
349
- console.log("\nOptional before first commit:");
350
- for (const step of flow.optionalOnboarding.steps) {
351
- console.log(` ${step}`);
352
- }
353
- console.log(`Note: ${flow.optionalOnboarding.note}`);
490
+ const payload = buildCreateCompletionPayload(flow);
491
+ if (emitOutput) {
492
+ printCompletionPayload(payload, {
493
+ printLine,
494
+ warnLine,
495
+ });
354
496
  }
497
+ return payload;
355
498
  } finally {
356
499
  if (activePrompt && activePrompt !== prompt) {
357
500
  activePrompt.close();
@@ -361,13 +504,15 @@ export async function executeCreateCommand({
361
504
 
362
505
  export async function executeAddCommand({
363
506
  cwd,
507
+ emitOutput = true,
364
508
  flags,
365
509
  kind,
366
510
  name,
367
- }: AddExecutionInput): Promise<void> {
511
+ printLine = console.log as PrintLine,
512
+ }: AddExecutionInput): Promise<AlternateBufferCompletionPayload | void> {
368
513
  if (!kind) {
369
514
  const { formatAddHelpText } = await loadCliAddRuntime();
370
- console.log(formatAddHelpText());
515
+ printLine(formatAddHelpText());
371
516
  return;
372
517
  }
373
518
 
@@ -390,8 +535,18 @@ export async function executeAddCommand({
390
535
  cwd,
391
536
  variationName: name,
392
537
  });
393
- console.log(`✅ Added variation ${result.variationSlug} to ${result.blockSlug} in ${result.projectDir}.`);
394
- 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;
395
550
  }
396
551
 
397
552
  if (kind === "pattern") {
@@ -405,8 +560,17 @@ export async function executeAddCommand({
405
560
  cwd,
406
561
  patternName: name,
407
562
  });
408
- console.log(`✅ Added pattern ${result.patternSlug} in ${result.projectDir}.`);
409
- 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;
410
574
  }
411
575
 
412
576
  if (kind === "binding-source") {
@@ -420,8 +584,17 @@ export async function executeAddCommand({
420
584
  bindingSourceName: name,
421
585
  cwd,
422
586
  });
423
- console.log(`✅ Added binding source ${result.bindingSourceSlug} in ${result.projectDir}.`);
424
- 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;
425
598
  }
426
599
 
427
600
  if (kind === "hooked-block") {
@@ -449,10 +622,19 @@ export async function executeAddCommand({
449
622
  cwd,
450
623
  position,
451
624
  });
452
- console.log(
453
- `✅ Added blockHooks metadata for ${result.blockSlug} relative to ${result.anchorBlockName} (${result.position}) in ${result.projectDir}.`,
454
- );
455
- 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;
456
638
  }
457
639
 
458
640
  if (kind !== "block") {
@@ -485,9 +667,18 @@ export async function executeAddCommand({
485
667
  | "compound",
486
668
  });
487
669
 
488
- console.log(
489
- `✅ Added ${result.blockSlugs.join(", ")} to ${result.projectDir} using the ${result.templateId} family.`,
490
- );
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;
491
682
  }
492
683
 
493
684
  export async function executeTemplatesCommand(
@@ -568,7 +759,7 @@ export async function executeMigrateCommand({
568
759
  flags,
569
760
  prompt,
570
761
  renderLine,
571
- }: MigrateExecutionInput): Promise<void> {
762
+ }: MigrateExecutionInput): Promise<AlternateBufferCompletionPayload | void> {
572
763
  const { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand } =
573
764
  await loadMigrationsRuntime();
574
765
  if (!command) {
@@ -599,10 +790,31 @@ export async function executeMigrateCommand({
599
790
  pushFlag(argv, "seed", readOptionalLooseStringFlag(flags, "seed"));
600
791
 
601
792
  const parsed = parseMigrationArgs(argv);
602
- 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, {
603
803
  prompt,
604
- renderLine,
804
+ renderLine: captureLine,
605
805
  });
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
+ });
813
+ }
814
+
815
+ if (result && typeof result === "object" && "cancelled" in result && result.cancelled === true) {
816
+ return;
817
+ }
606
818
  }
607
819
 
608
820
  export { listTemplates };
@@ -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";
@@ -137,7 +138,7 @@ function AddFlowFields({
137
138
  }: {
138
139
  workspaceBlockOptions: WorkspaceBlockOption[];
139
140
  }) {
140
- const { activeFieldName, values } = useFormContext();
141
+ const { activeFieldName, isSubmitting, values } = useFormContext();
141
142
  const { height: terminalHeight = 24 } = useTerminalDimensions();
142
143
  const addValues = values as Partial<AddFlowValues>;
143
144
  const kind = addValues.kind ?? "block";
@@ -160,8 +161,14 @@ function AddFlowFields({
160
161
  const variationBlockUsesSelect = kind === "variation" && workspaceBlockOptions.length > 0;
161
162
 
162
163
  return createElement(
163
- FirstPartyScrollBox,
164
- { scrollTop, viewportHeight },
164
+ FirstPartyFormViewport,
165
+ {
166
+ isSubmitting,
167
+ scrollTop,
168
+ submittingDescription: "Applying your workspace changes...",
169
+ submittingTitle: "Updating workspace...",
170
+ viewportHeight,
171
+ },
165
172
  [
166
173
  createElement(FirstPartySelectField, {
167
174
  ...getWrappedFieldNeighbors(orderedVisibleFields, "kind"),
@@ -257,9 +264,11 @@ function AddFlowFields({
257
264
  }
258
265
 
259
266
  export function AddFlow({ cwd, initialValues }: AddFlowProps) {
260
- const { handleCancel, handleFailure, handleSubmit } = useAlternateBufferLifecycle(
267
+ const { completion, handleCancel, handleFailure, handleSubmit, status } =
268
+ useAlternateBufferLifecycle(
261
269
  "wp-typia add failed",
262
- );
270
+ );
271
+ const { height: terminalHeight = 24 } = useTerminalDimensions();
263
272
  const [workspaceBlockOptions, setWorkspaceBlockOptions] = useState<WorkspaceBlockOption[]>([]);
264
273
 
265
274
  useEffect(() => {
@@ -283,6 +292,13 @@ export function AddFlow({ cwd, initialValues }: AddFlowProps) {
283
292
  };
284
293
  }, [cwd, handleFailure]);
285
294
 
295
+ if (status === "completed" && completion) {
296
+ return createElement(FirstPartyCompletionViewport, {
297
+ completion,
298
+ viewportHeight: getAddViewportHeight(terminalHeight),
299
+ });
300
+ }
301
+
286
302
  return (
287
303
  <Form
288
304
  initialValues={initialValues}
@@ -290,8 +306,9 @@ export function AddFlow({ cwd, initialValues }: AddFlowProps) {
290
306
  onSubmit={async (values) =>
291
307
  handleSubmit(async () => {
292
308
  const flags = sanitizeAddSubmitValues(values);
293
- await executeAddCommand({
309
+ return executeAddCommand({
294
310
  cwd,
311
+ emitOutput: false,
295
312
  flags,
296
313
  kind: values.kind,
297
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
  }