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 +4 -4
- package/src/commands/add.ts +1 -2
- package/src/commands/doctor.ts +3 -3
- package/src/runtime-bridge.ts +289 -68
- package/src/ui/add-flow.tsx +58 -16
- package/src/ui/alternate-buffer-lifecycle.ts +98 -12
- package/src/ui/create-flow.tsx +24 -6
- package/src/ui/first-party-form.tsx +260 -0
- package/src/ui/migrate-flow.tsx +24 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wp-typia",
|
|
3
|
-
"version": "0.16.
|
|
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.
|
|
68
|
+
"@wp-typia/project-tools": "0.16.4",
|
|
69
69
|
"better-result": "^2.7.0",
|
|
70
|
-
"react": "19.2.
|
|
71
|
-
"react-dom": "19.2.
|
|
70
|
+
"react": "^19.2.5",
|
|
71
|
+
"react-dom": "^19.2.5",
|
|
72
72
|
"zod": "4.3.6"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
package/src/commands/add.ts
CHANGED
|
@@ -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
|
|
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
|
},
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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
|
|
20
|
+
await executeDoctorCommand(args.cwd);
|
|
21
21
|
},
|
|
22
22
|
name: "doctor",
|
|
23
23
|
});
|
package/src/runtime-bridge.ts
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
511
|
+
printLine = console.log as PrintLine,
|
|
512
|
+
}: AddExecutionInput): Promise<AlternateBufferCompletionPayload | void> {
|
|
366
513
|
if (!kind) {
|
|
367
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
815
|
+
if (result && typeof result === "object" && "cancelled" in result && result.cancelled === true) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
597
818
|
}
|
|
598
819
|
|
|
599
|
-
export {
|
|
820
|
+
export { listTemplates };
|
package/src/ui/add-flow.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
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
|
-
|
|
163
|
-
{
|
|
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
|
|
259
|
-
const { handleCancel, handleSubmit } =
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
}
|
package/src/ui/create-flow.tsx
CHANGED
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
} from "./create-flow-model";
|
|
22
22
|
import {
|
|
23
23
|
FirstPartyCheckboxField,
|
|
24
|
-
|
|
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
|
-
|
|
92
|
-
{
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|
package/src/ui/migrate-flow.tsx
CHANGED
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
} from "./migrate-flow-model";
|
|
20
20
|
import {
|
|
21
21
|
FirstPartyCheckboxField,
|
|
22
|
-
|
|
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
|
-
|
|
102
|
-
{
|
|
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(
|
|
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
|
-
|
|
209
|
+
return executeMigrateCommand({
|
|
193
210
|
command: values.command,
|
|
194
211
|
cwd,
|
|
195
212
|
flags,
|
|
213
|
+
renderLine: () => undefined,
|
|
196
214
|
});
|
|
197
215
|
})
|
|
198
216
|
}
|