wp-typia 0.15.5 → 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/.bunli/commands.gen.ts +8 -1
- package/package.json +2 -2
- package/src/command-contract.ts +6 -0
- package/src/command-list.ts +2 -0
- package/src/commands/sync.ts +25 -0
- package/src/runtime-bridge.ts +162 -0
- package/src/ui/README.md +7 -0
- package/src/ui/add-flow.tsx +14 -26
- package/src/ui/alternate-buffer-lifecycle.ts +152 -0
- package/src/ui/create-flow.tsx +14 -30
- package/src/ui/lazy-flow.tsx +22 -17
- package/src/ui/migrate-flow.tsx +14 -26
package/.bunli/commands.gen.ts
CHANGED
|
@@ -9,10 +9,11 @@ import Create from '../src/commands/create.js'
|
|
|
9
9
|
import Doctor from '../src/commands/doctor.js'
|
|
10
10
|
import Mcp from '../src/commands/mcp.js'
|
|
11
11
|
import Migrate from '../src/commands/migrate.js'
|
|
12
|
+
import Sync from '../src/commands/sync.js'
|
|
12
13
|
import Templates from '../src/commands/templates.js'
|
|
13
14
|
|
|
14
15
|
// Narrow list of command names to avoid typeof-cycles in types
|
|
15
|
-
const names = ['add', 'create', 'doctor', 'mcp', 'migrate', 'templates'] as const
|
|
16
|
+
const names = ['add', 'create', 'doctor', 'mcp', 'migrate', 'sync', 'templates'] as const
|
|
16
17
|
type GeneratedNames = typeof names[number]
|
|
17
18
|
|
|
18
19
|
const modules: Record<GeneratedNames, Command<any>> = {
|
|
@@ -21,6 +22,7 @@ const modules: Record<GeneratedNames, Command<any>> = {
|
|
|
21
22
|
'doctor': Doctor,
|
|
22
23
|
'mcp': Mcp,
|
|
23
24
|
'migrate': Migrate,
|
|
25
|
+
'sync': Sync,
|
|
24
26
|
'templates': Templates
|
|
25
27
|
} as const
|
|
26
28
|
|
|
@@ -50,6 +52,11 @@ const metadata: Record<GeneratedNames, GeneratedCommandMeta> = {
|
|
|
50
52
|
description: 'Run migration workflows for migration-capable wp-typia projects.',
|
|
51
53
|
path: './src/commands/migrate'
|
|
52
54
|
},
|
|
55
|
+
'sync': {
|
|
56
|
+
name: 'sync',
|
|
57
|
+
description: 'Run the common generated-project sync workflow.',
|
|
58
|
+
path: './src/commands/sync'
|
|
59
|
+
},
|
|
53
60
|
'templates': {
|
|
54
61
|
name: 'templates',
|
|
55
62
|
description: 'Inspect built-in and external scaffold templates.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wp-typia",
|
|
3
|
-
"version": "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",
|
|
@@ -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.
|
|
68
|
+
"@wp-typia/project-tools": "0.16.0",
|
|
69
69
|
"better-result": "^2.7.0",
|
|
70
70
|
"react": "19.2.0",
|
|
71
71
|
"react-dom": "19.2.0",
|
package/src/command-contract.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const WP_TYPIA_BUNLI_MIGRATION_DOC = "docs/bunli-cli-migration.md";
|
|
|
6
6
|
|
|
7
7
|
export const WP_TYPIA_RESERVED_TOP_LEVEL_COMMAND_NAMES = [
|
|
8
8
|
"create",
|
|
9
|
+
"sync",
|
|
9
10
|
"add",
|
|
10
11
|
"migrate",
|
|
11
12
|
"templates",
|
|
@@ -18,6 +19,7 @@ export const WP_TYPIA_RESERVED_TOP_LEVEL_COMMAND_NAMES = [
|
|
|
18
19
|
|
|
19
20
|
export const WP_TYPIA_TOP_LEVEL_COMMAND_NAMES = [
|
|
20
21
|
"create",
|
|
22
|
+
"sync",
|
|
21
23
|
"add",
|
|
22
24
|
"migrate",
|
|
23
25
|
"templates",
|
|
@@ -108,6 +110,10 @@ export const WP_TYPIA_FUTURE_COMMAND_TREE = [
|
|
|
108
110
|
description: "Scaffold a new wp-typia project.",
|
|
109
111
|
name: "create",
|
|
110
112
|
},
|
|
113
|
+
{
|
|
114
|
+
description: "Run the common generated-project sync workflow.",
|
|
115
|
+
name: "sync",
|
|
116
|
+
},
|
|
111
117
|
{
|
|
112
118
|
description: "Extend an official wp-typia workspace.",
|
|
113
119
|
name: "add",
|
package/src/command-list.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { createCommand } from "./commands/create";
|
|
|
5
5
|
import { doctorCommand } from "./commands/doctor";
|
|
6
6
|
import { mcpCommand } from "./commands/mcp";
|
|
7
7
|
import { migrateCommand } from "./commands/migrate";
|
|
8
|
+
import { syncCommand } from "./commands/sync";
|
|
8
9
|
import { templatesCommand } from "./commands/templates";
|
|
9
10
|
|
|
10
11
|
export const wpTypiaCommands: Command<any, any>[] = [
|
|
11
12
|
createCommand,
|
|
13
|
+
syncCommand,
|
|
12
14
|
addCommand,
|
|
13
15
|
migrateCommand,
|
|
14
16
|
templatesCommand,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineCommand } from "@bunli/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { executeSyncCommand } from "../runtime-bridge";
|
|
5
|
+
|
|
6
|
+
export const syncCommand = defineCommand({
|
|
7
|
+
description: "Run the common generated-project sync workflow.",
|
|
8
|
+
handler: async (args) => {
|
|
9
|
+
await executeSyncCommand({
|
|
10
|
+
check: Boolean(args.flags.check),
|
|
11
|
+
cwd: args.cwd,
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
name: "sync",
|
|
15
|
+
options: {
|
|
16
|
+
check: {
|
|
17
|
+
argumentKind: "flag" as const,
|
|
18
|
+
description:
|
|
19
|
+
"Check generated artifacts without writing changes. Advanced sync-types-only flags stay on sync-types.",
|
|
20
|
+
schema: z.boolean().default(false),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export default syncCommand;
|
package/src/runtime-bridge.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
1
5
|
import {
|
|
2
6
|
createReadlinePrompt,
|
|
7
|
+
formatRunScript,
|
|
3
8
|
formatAddHelpText,
|
|
4
9
|
formatMigrationHelpText,
|
|
5
10
|
formatTemplateDetails,
|
|
@@ -44,6 +49,11 @@ type TemplatesExecutionInput = {
|
|
|
44
49
|
};
|
|
45
50
|
};
|
|
46
51
|
|
|
52
|
+
type SyncExecutionInput = {
|
|
53
|
+
check?: boolean;
|
|
54
|
+
cwd: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
47
57
|
type MigrateExecutionInput = {
|
|
48
58
|
command?: string;
|
|
49
59
|
cwd: string;
|
|
@@ -53,6 +63,14 @@ type MigrateExecutionInput = {
|
|
|
53
63
|
};
|
|
54
64
|
|
|
55
65
|
type PrintLine = (line: string) => void;
|
|
66
|
+
type PackageManagerId = "bun" | "npm" | "pnpm" | "yarn";
|
|
67
|
+
|
|
68
|
+
type SyncProjectContext = {
|
|
69
|
+
cwd: string;
|
|
70
|
+
packageJsonPath: string;
|
|
71
|
+
packageManager: PackageManagerId;
|
|
72
|
+
scripts: Partial<Record<"sync" | "sync-rest" | "sync-types", string>>;
|
|
73
|
+
};
|
|
56
74
|
|
|
57
75
|
function printBlock(lines: string[], printLine: PrintLine): void {
|
|
58
76
|
for (const line of lines) {
|
|
@@ -100,6 +118,131 @@ function pushFlag(argv: string[], name: string, value: unknown): void {
|
|
|
100
118
|
argv.push(`--${name}`, String(value));
|
|
101
119
|
}
|
|
102
120
|
|
|
121
|
+
function getSyncRootError(cwd: string): Error {
|
|
122
|
+
return new Error(
|
|
123
|
+
`No generated wp-typia project root was found at ${cwd}. Run \`wp-typia sync\` from a scaffolded project or official workspace root.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function inferSyncPackageManager(cwd: string, packageManagerField?: string): PackageManagerId {
|
|
128
|
+
const field = String(packageManagerField ?? "");
|
|
129
|
+
if (field.startsWith("bun@")) return "bun";
|
|
130
|
+
if (field.startsWith("npm@")) return "npm";
|
|
131
|
+
if (field.startsWith("pnpm@")) return "pnpm";
|
|
132
|
+
if (field.startsWith("yarn@")) return "yarn";
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
fs.existsSync(path.join(cwd, "bun.lock")) ||
|
|
136
|
+
fs.existsSync(path.join(cwd, "bun.lockb"))
|
|
137
|
+
) {
|
|
138
|
+
return "bun";
|
|
139
|
+
}
|
|
140
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
|
|
141
|
+
return "pnpm";
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
fs.existsSync(path.join(cwd, "yarn.lock")) ||
|
|
145
|
+
fs.existsSync(path.join(cwd, ".pnp.cjs")) ||
|
|
146
|
+
fs.existsSync(path.join(cwd, ".pnp.loader.mjs")) ||
|
|
147
|
+
fs.existsSync(path.join(cwd, ".yarnrc.yml"))
|
|
148
|
+
) {
|
|
149
|
+
return "yarn";
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
fs.existsSync(path.join(cwd, "package-lock.json")) ||
|
|
153
|
+
fs.existsSync(path.join(cwd, "npm-shrinkwrap.json"))
|
|
154
|
+
) {
|
|
155
|
+
return "npm";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return "npm";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resolveSyncProjectContext(cwd: string): SyncProjectContext {
|
|
162
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
163
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
164
|
+
throw getSyncRootError(cwd);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
168
|
+
packageManager?: string;
|
|
169
|
+
scripts?: Record<string, unknown>;
|
|
170
|
+
};
|
|
171
|
+
const scripts = packageJson.scripts ?? {};
|
|
172
|
+
const syncScripts = {
|
|
173
|
+
sync: typeof scripts.sync === "string" ? scripts.sync : undefined,
|
|
174
|
+
"sync-rest":
|
|
175
|
+
typeof scripts["sync-rest"] === "string" ? scripts["sync-rest"] : undefined,
|
|
176
|
+
"sync-types":
|
|
177
|
+
typeof scripts["sync-types"] === "string" ? scripts["sync-types"] : undefined,
|
|
178
|
+
} satisfies SyncProjectContext["scripts"];
|
|
179
|
+
|
|
180
|
+
if (!syncScripts.sync && !syncScripts["sync-types"]) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Expected ${packageJsonPath} to define either a \`sync\` or \`sync-types\` script.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
cwd,
|
|
188
|
+
packageJsonPath,
|
|
189
|
+
packageManager: inferSyncPackageManager(cwd, packageJson.packageManager),
|
|
190
|
+
scripts: syncScripts,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getPackageManagerRunInvocation(
|
|
195
|
+
packageManager: PackageManagerId,
|
|
196
|
+
scriptName: string,
|
|
197
|
+
extraArgs: string[],
|
|
198
|
+
): { args: string[]; command: string } {
|
|
199
|
+
switch (packageManager) {
|
|
200
|
+
case "bun":
|
|
201
|
+
return { args: ["run", scriptName, ...extraArgs], command: "bun" };
|
|
202
|
+
case "npm":
|
|
203
|
+
return {
|
|
204
|
+
args: ["run", scriptName, ...(extraArgs.length > 0 ? ["--", ...extraArgs] : [])],
|
|
205
|
+
command: "npm",
|
|
206
|
+
};
|
|
207
|
+
case "pnpm":
|
|
208
|
+
return { args: ["run", scriptName, ...extraArgs], command: "pnpm" };
|
|
209
|
+
case "yarn":
|
|
210
|
+
return { args: ["run", scriptName, ...extraArgs], command: "yarn" };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runProjectScript(
|
|
215
|
+
project: SyncProjectContext,
|
|
216
|
+
scriptName: "sync" | "sync-rest" | "sync-types",
|
|
217
|
+
extraArgs: string[],
|
|
218
|
+
): void {
|
|
219
|
+
const script = project.scripts[scriptName];
|
|
220
|
+
if (!script) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const invocation = getPackageManagerRunInvocation(
|
|
225
|
+
project.packageManager,
|
|
226
|
+
scriptName,
|
|
227
|
+
extraArgs,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
231
|
+
cwd: project.cwd,
|
|
232
|
+
shell: process.platform === "win32",
|
|
233
|
+
stdio: "inherit",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (result.error || result.status !== 0) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`\`${formatRunScript(project.packageManager, scriptName, extraArgs.join(" "))}\` failed.`,
|
|
239
|
+
{
|
|
240
|
+
cause: result.error,
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
103
246
|
const PACKAGE_MANAGER_PROMPT_OPTIONS = [
|
|
104
247
|
{ label: "npm", value: "npm", hint: "Use npm" },
|
|
105
248
|
{ label: "pnpm", value: "pnpm", hint: "Use pnpm" },
|
|
@@ -384,6 +527,25 @@ export async function executeDoctorCommand(cwd: string): Promise<void> {
|
|
|
384
527
|
await runDoctor(cwd);
|
|
385
528
|
}
|
|
386
529
|
|
|
530
|
+
export async function executeSyncCommand({
|
|
531
|
+
check = false,
|
|
532
|
+
cwd,
|
|
533
|
+
}: SyncExecutionInput): Promise<void> {
|
|
534
|
+
const project = resolveSyncProjectContext(cwd);
|
|
535
|
+
const extraArgs = check ? ["--check"] : [];
|
|
536
|
+
|
|
537
|
+
if (project.scripts.sync) {
|
|
538
|
+
runProjectScript(project, "sync", extraArgs);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
runProjectScript(project, "sync-types", extraArgs);
|
|
543
|
+
|
|
544
|
+
if (project.scripts["sync-rest"]) {
|
|
545
|
+
runProjectScript(project, "sync-rest", extraArgs);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
387
549
|
export async function executeMigrateCommand({
|
|
388
550
|
command,
|
|
389
551
|
cwd,
|
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.
|
package/src/ui/add-flow.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
|
45
|
-
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
42
|
+
const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia add failed");
|
|
46
43
|
|
|
47
44
|
return (
|
|
48
|
-
|
|
49
|
-
{
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
package/src/ui/create-flow.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
}
|
package/src/ui/lazy-flow.tsx
CHANGED
|
@@ -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
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
package/src/ui/migrate-flow.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
52
|
-
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
49
|
+
const { handleCancel, handleSubmit } = useAlternateBufferLifecycle("wp-typia migrate failed");
|
|
53
50
|
|
|
54
51
|
return (
|
|
55
|
-
|
|
56
|
-
{
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
}
|