wp-typia 0.16.0 → 0.16.1

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.0",
3
+ "version": "0.16.1",
4
4
  "description": "Canonical CLI package for wp-typia scaffolding and project workflows",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
package/src/ui/README.md CHANGED
@@ -15,3 +15,10 @@ Constraints:
15
15
  shell automation.
16
16
  - `@wp-typia/project-tools` remains the runtime library; these screens should only
17
17
  collect input and hand the resolved command state back to `wp-typia`.
18
+
19
+ Alternate-buffer lifecycle contract:
20
+
21
+ - `LazyFlow` owns pre-mount lifecycle safety for lazy loader failures and loading-time quit.
22
+ - Mounted flows must use the shared alternate-buffer lifecycle helper instead of ad hoc exit logic.
23
+ - `create`, `add`, and `migrate` must always call `runtime.exit()` on submit success, cancel, and quit.
24
+ - Runtime execution failures use exit-on-failure: report the error, then exit immediately.
@@ -1,11 +1,9 @@
1
- import { useState } from "react";
2
-
3
- import { useRuntime } from "@bunli/runtime/app";
4
- import { Alert, SchemaForm } from "@bunli/tui";
1
+ import { SchemaForm } from "@bunli/tui";
5
2
  import { HOOKED_BLOCK_POSITION_IDS } from "@wp-typia/project-tools";
6
3
  import { z } from "zod";
7
4
 
8
5
  import { executeAddCommand } from "../runtime-bridge";
6
+ import { useAlternateBufferLifecycle } from "./alternate-buffer-lifecycle";
9
7
 
10
8
  const addFlowSchema = z.object({
11
9
  anchor: z.string().optional(),
@@ -41,16 +39,11 @@ const HOOKED_BLOCK_POSITION_DESCRIPTIONS: Record<
41
39
  };
42
40
 
43
41
  export function AddFlow({ cwd, initialValues, workspaceBlockOptions }: AddFlowProps) {
44
- const runtime = useRuntime();
45
- const [errorMessage, setErrorMessage] = useState<string | null>(null);
42
+ const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia add failed");
46
43
 
47
44
  return (
48
- <>
49
- {errorMessage ? (
50
- <Alert message={errorMessage} title="Add failed" tone="danger" />
51
- ) : null}
52
- <SchemaForm
53
- fields={[
45
+ <SchemaForm
46
+ fields={[
54
47
  {
55
48
  kind: "select",
56
49
  label: "Kind",
@@ -195,25 +188,20 @@ export function AddFlow({ cwd, initialValues, workspaceBlockOptions }: AddFlowPr
195
188
  (values.template === "persistence" || values.template === "compound"),
196
189
  },
197
190
  ]}
198
- initialValues={initialValues}
199
- onCancel={() => runtime.exit()}
200
- onSubmit={async (values) => {
201
- try {
202
- setErrorMessage(null);
191
+ initialValues={initialValues}
192
+ onCancel={handleCancel}
193
+ onSubmit={async (values) =>
194
+ handleSubmit(async () => {
203
195
  await executeAddCommand({
204
196
  cwd,
205
197
  flags: values,
206
198
  kind: values.kind,
207
199
  name: values.name,
208
200
  });
209
- runtime.exit();
210
- } catch (error) {
211
- setErrorMessage(error instanceof Error ? error.message : String(error));
212
- }
213
- }}
214
- schema={addFlowSchema}
215
- title="Extend a wp-typia workspace"
216
- />
217
- </>
201
+ })
202
+ }
203
+ schema={addFlowSchema}
204
+ title="Extend a wp-typia workspace"
205
+ />
218
206
  );
219
207
  }
@@ -0,0 +1,152 @@
1
+ import { useCallback } from "react";
2
+
3
+ import { useRuntime } from "@bunli/runtime/app";
4
+ import { useKeyboard } from "@bunli/tui";
5
+
6
+ type AlternateBufferKeyEvent = {
7
+ ctrl?: boolean;
8
+ name?: string;
9
+ };
10
+
11
+ type AlternateBufferFailureOptions = {
12
+ context: string;
13
+ error: unknown;
14
+ exit: () => void;
15
+ log?: (message: string) => void;
16
+ };
17
+
18
+ type RunAlternateBufferActionOptions = {
19
+ action: () => Promise<void>;
20
+ context: string;
21
+ exit: () => void;
22
+ log?: (message: string) => void;
23
+ };
24
+
25
+ export function describeAlternateBufferFailure(context: string, error: unknown): string {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ return `${context}: ${message}`;
28
+ }
29
+
30
+ export function isAlternateBufferExitKey(key: AlternateBufferKeyEvent): boolean {
31
+ return key.name === "q" || (key.ctrl === true && key.name === "c");
32
+ }
33
+
34
+ export function reportAlternateBufferFailure({
35
+ context,
36
+ error,
37
+ exit,
38
+ log = console.error,
39
+ }: AlternateBufferFailureOptions): void {
40
+ const message = describeAlternateBufferFailure(context, error);
41
+ exit();
42
+ log(message);
43
+ }
44
+
45
+ export async function runAlternateBufferAction({
46
+ action,
47
+ context,
48
+ exit,
49
+ log = console.error,
50
+ }: RunAlternateBufferActionOptions): Promise<void> {
51
+ try {
52
+ await action();
53
+ exit();
54
+ } catch (error) {
55
+ reportAlternateBufferFailure({ context, error, exit, log });
56
+ }
57
+ }
58
+
59
+ export async function resolveLazyFlowComponent<TProps>({
60
+ loader,
61
+ onLoaded,
62
+ onFailure,
63
+ isDisposed,
64
+ }: {
65
+ loader: () => Promise<{ default: React.ComponentType<TProps> }>;
66
+ onLoaded: (component: React.ComponentType<TProps>) => void;
67
+ onFailure: (error: unknown) => void;
68
+ isDisposed: () => boolean;
69
+ }): Promise<void> {
70
+ try {
71
+ const module = await loader();
72
+ if (!isDisposed()) {
73
+ onLoaded(module.default);
74
+ }
75
+ } catch (error) {
76
+ if (!isDisposed()) {
77
+ onFailure(error);
78
+ }
79
+ }
80
+ }
81
+
82
+ export function useAlternateBufferExitKeys(options: {
83
+ enabled?: boolean;
84
+ exit?: () => void;
85
+ } = {}): void {
86
+ const runtime = useRuntime();
87
+ const exit = options.exit ?? (() => runtime.exit());
88
+ const enabled = options.enabled ?? true;
89
+
90
+ useKeyboard((key: AlternateBufferKeyEvent) => {
91
+ if (!enabled) {
92
+ return;
93
+ }
94
+
95
+ if (isAlternateBufferExitKey(key)) {
96
+ exit();
97
+ }
98
+ });
99
+ }
100
+
101
+ export function useAlternateBufferLifecycle(
102
+ context: string,
103
+ options: {
104
+ enableExitKeys?: boolean;
105
+ } = {},
106
+ ): {
107
+ handleCancel: () => void;
108
+ handleFailure: (error: unknown) => void;
109
+ handleSubmit: (action: () => Promise<void>) => Promise<void>;
110
+ } {
111
+ const runtime = useRuntime();
112
+ const exit = useCallback(() => {
113
+ runtime.exit();
114
+ }, [runtime]);
115
+
116
+ useAlternateBufferExitKeys({
117
+ enabled: options.enableExitKeys ?? true,
118
+ exit,
119
+ });
120
+
121
+ const handleCancel = useCallback(() => {
122
+ exit();
123
+ }, [exit]);
124
+
125
+ const handleFailure = useCallback(
126
+ (error: unknown) => {
127
+ reportAlternateBufferFailure({
128
+ context,
129
+ error,
130
+ exit,
131
+ });
132
+ },
133
+ [context, exit],
134
+ );
135
+
136
+ const handleSubmit = useCallback(
137
+ async (action: () => Promise<void>) => {
138
+ await runAlternateBufferAction({
139
+ action,
140
+ context,
141
+ exit,
142
+ });
143
+ },
144
+ [context, exit],
145
+ );
146
+
147
+ return {
148
+ handleCancel,
149
+ handleFailure,
150
+ handleSubmit,
151
+ };
152
+ }
@@ -1,10 +1,8 @@
1
- import { useState } from "react";
2
-
3
- import { useRuntime } from "@bunli/runtime/app";
4
- import { Alert, SchemaForm } from "@bunli/tui";
1
+ import { SchemaForm } from "@bunli/tui";
5
2
  import { z } from "zod";
6
3
 
7
4
  import { executeCreateCommand } from "../runtime-bridge";
5
+ import { useAlternateBufferLifecycle } from "./alternate-buffer-lifecycle";
8
6
 
9
7
  const createFlowSchema = z.object({
10
8
  "data-storage": z.string().optional(),
@@ -31,8 +29,7 @@ type CreateFlowProps = {
31
29
  };
32
30
 
33
31
  export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
34
- const runtime = useRuntime();
35
- const [errorMessage, setErrorMessage] = useState<string | null>(null);
32
+ const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia create failed");
36
33
  const defaultPrompt = {
37
34
  close() {},
38
35
  select<T extends string>(_message: string, options: Array<{ value: T }>, defaultValue = 1) {
@@ -45,16 +42,8 @@ export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
45
42
  };
46
43
 
47
44
  return (
48
- <>
49
- {errorMessage ? (
50
- <Alert
51
- message={errorMessage}
52
- title="Create failed"
53
- tone="danger"
54
- />
55
- ) : null}
56
- <SchemaForm
57
- fields={[
45
+ <SchemaForm
46
+ fields={[
58
47
  { kind: "text", label: "Project directory", name: "project-dir", required: true },
59
48
  {
60
49
  kind: "select",
@@ -146,11 +135,10 @@ export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
146
135
  name: "with-migration-ui",
147
136
  },
148
137
  ]}
149
- initialValues={initialValues}
150
- onCancel={() => runtime.exit()}
151
- onSubmit={async (values) => {
152
- try {
153
- setErrorMessage(null);
138
+ initialValues={initialValues}
139
+ onCancel={handleCancel}
140
+ onSubmit={async (values) =>
141
+ handleSubmit(async () => {
154
142
  await executeCreateCommand({
155
143
  cwd,
156
144
  flags: values,
@@ -158,14 +146,10 @@ export function CreateFlow({ cwd, initialValues }: CreateFlowProps) {
158
146
  projectDir: values["project-dir"],
159
147
  prompt: defaultPrompt,
160
148
  });
161
- runtime.exit();
162
- } catch (error) {
163
- setErrorMessage(error instanceof Error ? error.message : String(error));
164
- }
165
- }}
166
- schema={createFlowSchema}
167
- title="Create a wp-typia project"
168
- />
169
- </>
149
+ })
150
+ }
151
+ schema={createFlowSchema}
152
+ title="Create a wp-typia project"
153
+ />
170
154
  );
171
155
  }
@@ -1,5 +1,11 @@
1
1
  import { createElement, useEffect, useState, type ComponentType } from "react";
2
2
 
3
+ import {
4
+ resolveLazyFlowComponent,
5
+ useAlternateBufferExitKeys,
6
+ useAlternateBufferLifecycle,
7
+ } from "./alternate-buffer-lifecycle";
8
+
3
9
  type LazyFlowProps<TProps> = {
4
10
  loader: () => Promise<{ default: ComponentType<TProps> }>;
5
11
  props: TProps;
@@ -7,31 +13,30 @@ type LazyFlowProps<TProps> = {
7
13
 
8
14
  export function LazyFlow<TProps>({ loader, props }: LazyFlowProps<TProps>) {
9
15
  const [Component, setComponent] = useState<ComponentType<TProps> | null>(null);
10
- const [errorMessage, setErrorMessage] = useState<string | null>(null);
16
+ const { handleFailure } = useAlternateBufferLifecycle("wp-typia TUI flow failed", {
17
+ enableExitKeys: false,
18
+ });
19
+
20
+ useAlternateBufferExitKeys({
21
+ enabled: Component === null,
22
+ });
11
23
 
12
24
  useEffect(() => {
13
25
  let disposed = false;
14
26
 
15
- void loader()
16
- .then((module) => {
17
- if (!disposed) {
18
- setComponent(() => module.default);
19
- }
20
- })
21
- .catch((error) => {
22
- if (!disposed) {
23
- setErrorMessage(error instanceof Error ? error.message : String(error));
24
- }
25
- });
27
+ void resolveLazyFlowComponent({
28
+ isDisposed: () => disposed,
29
+ loader,
30
+ onFailure: handleFailure,
31
+ onLoaded: (component) => {
32
+ setComponent(() => component);
33
+ },
34
+ });
26
35
 
27
36
  return () => {
28
37
  disposed = true;
29
38
  };
30
- }, [loader]);
31
-
32
- if (errorMessage) {
33
- return errorMessage;
34
- }
39
+ }, [handleFailure, loader]);
35
40
 
36
41
  if (!Component) {
37
42
  return null;
@@ -1,10 +1,8 @@
1
- import { useState } from "react";
2
-
3
- import { useRuntime } from "@bunli/runtime/app";
4
- import { Alert, SchemaForm } from "@bunli/tui";
1
+ import { SchemaForm } from "@bunli/tui";
5
2
  import { z } from "zod";
6
3
 
7
4
  import { executeMigrateCommand } from "../runtime-bridge";
5
+ import { useAlternateBufferLifecycle } from "./alternate-buffer-lifecycle";
8
6
 
9
7
  const migrateFlowSchema = z.object({
10
8
  all: z.boolean().default(false),
@@ -48,16 +46,11 @@ function sanitizeMigrateValues(values: MigrateFlowValues): Record<string, unknow
48
46
  }
49
47
 
50
48
  export function MigrateFlow({ cwd, initialValues }: MigrateFlowProps) {
51
- const runtime = useRuntime();
52
- const [errorMessage, setErrorMessage] = useState<string | null>(null);
49
+ const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia migrate failed");
53
50
 
54
51
  return (
55
- <>
56
- {errorMessage ? (
57
- <Alert message={errorMessage} title="Migrate failed" tone="danger" />
58
- ) : null}
59
- <SchemaForm
60
- fields={[
52
+ <SchemaForm
53
+ fields={[
61
54
  {
62
55
  kind: "select",
63
56
  label: "Migration command",
@@ -164,24 +157,19 @@ export function MigrateFlow({ cwd, initialValues }: MigrateFlowProps) {
164
157
  visibleWhen: (values) => values.command === "fuzz",
165
158
  },
166
159
  ]}
167
- initialValues={initialValues}
168
- onCancel={() => runtime.exit()}
169
- onSubmit={async (values) => {
170
- try {
171
- setErrorMessage(null);
160
+ initialValues={initialValues}
161
+ onCancel={handleCancel}
162
+ onSubmit={async (values) =>
163
+ handleSubmit(async () => {
172
164
  await executeMigrateCommand({
173
165
  command: values.command,
174
166
  cwd,
175
167
  flags: sanitizeMigrateValues(values),
176
168
  });
177
- runtime.exit();
178
- } catch (error) {
179
- setErrorMessage(error instanceof Error ? error.message : String(error));
180
- }
181
- }}
182
- schema={migrateFlowSchema}
183
- title="Run wp-typia migration workflows"
184
- />
185
- </>
169
+ })
170
+ }
171
+ schema={migrateFlowSchema}
172
+ title="Run wp-typia migration workflows"
173
+ />
186
174
  );
187
175
  }