wp-typia 0.16.1 → 0.16.3
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 +42 -33
- package/src/ui/README.md +19 -0
- package/src/ui/add-flow-model.ts +120 -0
- package/src/ui/add-flow.tsx +265 -165
- package/src/ui/create-flow-model.ts +125 -0
- package/src/ui/create-flow.tsx +163 -120
- package/src/ui/first-party-form-model.ts +62 -0
- package/src/ui/first-party-form.tsx +460 -0
- package/src/ui/migrate-flow-model.ts +126 -0
- package/src/ui/migrate-flow.tsx +169 -139
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wp-typia",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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.2",
|
|
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,29 +3,15 @@ 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
|
-
runAddHookedBlockCommand,
|
|
22
|
-
runAddPatternCommand,
|
|
23
|
-
runAddVariationCommand,
|
|
24
|
-
runDoctor,
|
|
25
|
-
runMigrationCommand,
|
|
26
|
-
runScaffoldFlow,
|
|
27
|
-
} from "@wp-typia/project-tools";
|
|
28
|
-
import type { ReadlinePrompt } from "@wp-typia/project-tools";
|
|
11
|
+
} from "@wp-typia/project-tools/cli-templates";
|
|
12
|
+
import { formatRunScript } from "@wp-typia/project-tools/package-managers";
|
|
13
|
+
import { tryResolveWorkspaceProject } from "@wp-typia/project-tools/workspace-project";
|
|
14
|
+
import type { ReadlinePrompt } from "@wp-typia/project-tools/cli-prompt";
|
|
29
15
|
|
|
30
16
|
type CreateExecutionInput = {
|
|
31
17
|
projectDir: string;
|
|
@@ -72,6 +58,13 @@ type SyncProjectContext = {
|
|
|
72
58
|
scripts: Partial<Record<"sync" | "sync-rest" | "sync-types", string>>;
|
|
73
59
|
};
|
|
74
60
|
|
|
61
|
+
const loadCliAddRuntime = () => import("@wp-typia/project-tools/cli-add");
|
|
62
|
+
const loadCliDoctorRuntime = () => import("@wp-typia/project-tools/cli-doctor");
|
|
63
|
+
const loadCliPromptRuntime = () => import("@wp-typia/project-tools/cli-prompt");
|
|
64
|
+
const loadCliScaffoldRuntime = () => import("@wp-typia/project-tools/cli-scaffold");
|
|
65
|
+
const loadCliTemplatesRuntime = () => import("@wp-typia/project-tools/cli-templates");
|
|
66
|
+
const loadMigrationsRuntime = () => import("@wp-typia/project-tools/migrations");
|
|
67
|
+
|
|
75
68
|
function printBlock(lines: string[], printLine: PrintLine): void {
|
|
76
69
|
for (const line of lines) {
|
|
77
70
|
printLine(line);
|
|
@@ -272,6 +265,15 @@ export async function executeCreateCommand({
|
|
|
272
265
|
interactive,
|
|
273
266
|
prompt,
|
|
274
267
|
}: CreateExecutionInput): Promise<void> {
|
|
268
|
+
const [
|
|
269
|
+
{ createReadlinePrompt },
|
|
270
|
+
{ runScaffoldFlow },
|
|
271
|
+
{ getTemplateSelectOptions },
|
|
272
|
+
] = await Promise.all([
|
|
273
|
+
loadCliPromptRuntime(),
|
|
274
|
+
loadCliScaffoldRuntime(),
|
|
275
|
+
loadCliTemplatesRuntime(),
|
|
276
|
+
]);
|
|
275
277
|
const shouldPrompt =
|
|
276
278
|
interactive ?? (!Boolean(flags.yes) && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY));
|
|
277
279
|
const activePrompt = shouldPrompt ? (prompt ?? createReadlinePrompt()) : undefined;
|
|
@@ -364,10 +366,13 @@ export async function executeAddCommand({
|
|
|
364
366
|
name,
|
|
365
367
|
}: AddExecutionInput): Promise<void> {
|
|
366
368
|
if (!kind) {
|
|
369
|
+
const { formatAddHelpText } = await loadCliAddRuntime();
|
|
367
370
|
console.log(formatAddHelpText());
|
|
368
371
|
return;
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
const addRuntime = await loadCliAddRuntime();
|
|
375
|
+
|
|
371
376
|
if (kind === "variation") {
|
|
372
377
|
if (!name) {
|
|
373
378
|
throw new Error(
|
|
@@ -380,7 +385,7 @@ export async function executeAddCommand({
|
|
|
380
385
|
throw new Error("`wp-typia add variation` requires --block <block-slug>.");
|
|
381
386
|
}
|
|
382
387
|
|
|
383
|
-
const result = await runAddVariationCommand({
|
|
388
|
+
const result = await addRuntime.runAddVariationCommand({
|
|
384
389
|
blockName: blockSlug,
|
|
385
390
|
cwd,
|
|
386
391
|
variationName: name,
|
|
@@ -396,7 +401,7 @@ export async function executeAddCommand({
|
|
|
396
401
|
);
|
|
397
402
|
}
|
|
398
403
|
|
|
399
|
-
const result = await runAddPatternCommand({
|
|
404
|
+
const result = await addRuntime.runAddPatternCommand({
|
|
400
405
|
cwd,
|
|
401
406
|
patternName: name,
|
|
402
407
|
});
|
|
@@ -411,7 +416,7 @@ export async function executeAddCommand({
|
|
|
411
416
|
);
|
|
412
417
|
}
|
|
413
418
|
|
|
414
|
-
const result = await runAddBindingSourceCommand({
|
|
419
|
+
const result = await addRuntime.runAddBindingSourceCommand({
|
|
415
420
|
bindingSourceName: name,
|
|
416
421
|
cwd,
|
|
417
422
|
});
|
|
@@ -438,7 +443,7 @@ export async function executeAddCommand({
|
|
|
438
443
|
);
|
|
439
444
|
}
|
|
440
445
|
|
|
441
|
-
const result = await runAddHookedBlockCommand({
|
|
446
|
+
const result = await addRuntime.runAddHookedBlockCommand({
|
|
442
447
|
anchorBlockName,
|
|
443
448
|
blockName: name,
|
|
444
449
|
cwd,
|
|
@@ -468,7 +473,7 @@ export async function executeAddCommand({
|
|
|
468
473
|
);
|
|
469
474
|
}
|
|
470
475
|
|
|
471
|
-
const result = await runAddBlockCommand({
|
|
476
|
+
const result = await addRuntime.runAddBlockCommand({
|
|
472
477
|
blockName: name,
|
|
473
478
|
cwd,
|
|
474
479
|
dataStorageMode: readOptionalStringFlag(flags, "data-storage"),
|
|
@@ -524,9 +529,20 @@ export async function executeTemplatesCommand(
|
|
|
524
529
|
}
|
|
525
530
|
|
|
526
531
|
export async function executeDoctorCommand(cwd: string): Promise<void> {
|
|
532
|
+
const { runDoctor } = await loadCliDoctorRuntime();
|
|
527
533
|
await runDoctor(cwd);
|
|
528
534
|
}
|
|
529
535
|
|
|
536
|
+
export async function loadAddWorkspaceBlockOptions(cwd: string) {
|
|
537
|
+
const workspace = tryResolveWorkspaceProject(cwd);
|
|
538
|
+
if (!workspace) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const { getWorkspaceBlockSelectOptions } = await loadCliAddRuntime();
|
|
543
|
+
return getWorkspaceBlockSelectOptions(workspace.projectDir);
|
|
544
|
+
}
|
|
545
|
+
|
|
530
546
|
export async function executeSyncCommand({
|
|
531
547
|
check = false,
|
|
532
548
|
cwd,
|
|
@@ -553,6 +569,8 @@ export async function executeMigrateCommand({
|
|
|
553
569
|
prompt,
|
|
554
570
|
renderLine,
|
|
555
571
|
}: MigrateExecutionInput): Promise<void> {
|
|
572
|
+
const { formatMigrationHelpText, parseMigrationArgs, runMigrationCommand } =
|
|
573
|
+
await loadMigrationsRuntime();
|
|
556
574
|
if (!command) {
|
|
557
575
|
console.log(formatMigrationHelpText());
|
|
558
576
|
return;
|
|
@@ -587,13 +605,4 @@ export async function executeMigrateCommand({
|
|
|
587
605
|
});
|
|
588
606
|
}
|
|
589
607
|
|
|
590
|
-
export
|
|
591
|
-
const workspace = tryResolveWorkspaceProject(cwd);
|
|
592
|
-
if (!workspace) {
|
|
593
|
-
return [];
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
return getWorkspaceBlockSelectOptions(workspace.projectDir);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export { formatAddHelpText, formatMigrationHelpText, listTemplates };
|
|
608
|
+
export { listTemplates };
|
package/src/ui/README.md
CHANGED
|
@@ -22,3 +22,22 @@ Alternate-buffer lifecycle contract:
|
|
|
22
22
|
- Mounted flows must use the shared alternate-buffer lifecycle helper instead of ad hoc exit logic.
|
|
23
23
|
- `create`, `add`, and `migrate` must always call `runtime.exit()` on submit success, cancel, and quit.
|
|
24
24
|
- Runtime execution failures use exit-on-failure: report the error, then exit immediately.
|
|
25
|
+
|
|
26
|
+
First-party form interaction contract:
|
|
27
|
+
|
|
28
|
+
- `create`, `add`, and `migrate` use a shared first-party form layer instead of relying on `SchemaForm` field traversal.
|
|
29
|
+
- Small viewport safety is part of the `wp-typia` contract: active fields must stay inside a scrollable viewport and footer help must not overlap the form body.
|
|
30
|
+
- Keyboard traversal is owned locally by `wp-typia`.
|
|
31
|
+
- `Tab` / `Shift+Tab` move across the visible field order.
|
|
32
|
+
- Select fields handle arrow-key movement and preserve traversal when moving back out to the next field.
|
|
33
|
+
- Checkbox fields use `Space` / `Enter` to toggle without trapping focus.
|
|
34
|
+
- Hidden conditional fields must never keep focus or leak stale values into submit payloads.
|
|
35
|
+
- `Ctrl+S` submit must stay reachable through the shared first-party field layer, not only through Bunli's form-level hotkeys.
|
|
36
|
+
|
|
37
|
+
Regression and triage contract:
|
|
38
|
+
|
|
39
|
+
- The committed regression contract is the first-party model and layout test suite under `packages/wp-typia/tests`.
|
|
40
|
+
- `create-flow-layout.test.ts` keeps the small-viewport scroll model and checkbox-cluster ordering pinned.
|
|
41
|
+
- `tui-interaction-models.test.ts` keeps visible field ordering and submit sanitization pinned for `add` and `migrate`.
|
|
42
|
+
- The committed tests deliberately stop short of a repo-level PTY smoke because the alternate-buffer harness cost and flake surface outweighed the regression value for this round.
|
|
43
|
+
- If a future regression needs terminal-level confirmation, reproduce it from the real `wp-typia` command first before classifying it as a Bunli-level issue.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
5
|
+
FIRST_PARTY_TEXT_FIELD_BODY_HEIGHT,
|
|
6
|
+
getFirstPartyScrollTop,
|
|
7
|
+
getFirstPartyViewportHeight,
|
|
8
|
+
} from "./first-party-form-model";
|
|
9
|
+
|
|
10
|
+
export const addFlowSchema = z.object({
|
|
11
|
+
anchor: z.string().optional(),
|
|
12
|
+
block: z.string().optional(),
|
|
13
|
+
"data-storage": z.string().optional(),
|
|
14
|
+
kind: z
|
|
15
|
+
.enum(["block", "variation", "pattern", "binding-source", "hooked-block"])
|
|
16
|
+
.default("block"),
|
|
17
|
+
name: z.string().optional(),
|
|
18
|
+
"persistence-policy": z.string().optional(),
|
|
19
|
+
position: z.string().optional(),
|
|
20
|
+
template: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type AddFlowValues = z.infer<typeof addFlowSchema>;
|
|
24
|
+
|
|
25
|
+
export type AddFieldName =
|
|
26
|
+
| "kind"
|
|
27
|
+
| "name"
|
|
28
|
+
| "template"
|
|
29
|
+
| "block"
|
|
30
|
+
| "anchor"
|
|
31
|
+
| "position"
|
|
32
|
+
| "data-storage"
|
|
33
|
+
| "persistence-policy";
|
|
34
|
+
|
|
35
|
+
const ADD_FIELD_ORDER = [
|
|
36
|
+
"kind",
|
|
37
|
+
"name",
|
|
38
|
+
"template",
|
|
39
|
+
"block",
|
|
40
|
+
"anchor",
|
|
41
|
+
"position",
|
|
42
|
+
"data-storage",
|
|
43
|
+
"persistence-policy",
|
|
44
|
+
] as const satisfies ReadonlyArray<AddFieldName>;
|
|
45
|
+
|
|
46
|
+
const ADD_FIELD_HEIGHTS: Record<AddFieldName, number> = {
|
|
47
|
+
anchor: FIRST_PARTY_TEXT_FIELD_BODY_HEIGHT,
|
|
48
|
+
block: FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
49
|
+
"data-storage": FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
50
|
+
kind: FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
51
|
+
name: FIRST_PARTY_TEXT_FIELD_BODY_HEIGHT,
|
|
52
|
+
"persistence-policy": FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
53
|
+
position: FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
54
|
+
template: FIRST_PARTY_SELECT_FIELD_BODY_HEIGHT,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function isAddPersistenceTemplate(template?: string): boolean {
|
|
58
|
+
return template === "persistence" || template === "compound";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getVisibleAddFieldNames(values: Partial<AddFlowValues>): Array<AddFieldName> {
|
|
62
|
+
switch (values.kind ?? "block") {
|
|
63
|
+
case "variation":
|
|
64
|
+
return ["kind", "name", "block"];
|
|
65
|
+
case "pattern":
|
|
66
|
+
return ["kind", "name"];
|
|
67
|
+
case "binding-source":
|
|
68
|
+
return ["kind", "name"];
|
|
69
|
+
case "hooked-block":
|
|
70
|
+
return ["kind", "name", "anchor", "position"];
|
|
71
|
+
case "block":
|
|
72
|
+
default:
|
|
73
|
+
return ADD_FIELD_ORDER.filter((name) => {
|
|
74
|
+
if (name === "data-storage" || name === "persistence-policy") {
|
|
75
|
+
return isAddPersistenceTemplate(values.template);
|
|
76
|
+
}
|
|
77
|
+
return name === "kind" || name === "name" || name === "template";
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getAddViewportHeight(terminalHeight = 24): number {
|
|
83
|
+
return getFirstPartyViewportHeight(terminalHeight);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getAddScrollTop(options: {
|
|
87
|
+
activeFieldName: string | null;
|
|
88
|
+
values: Partial<AddFlowValues>;
|
|
89
|
+
viewportHeight: number;
|
|
90
|
+
}): number {
|
|
91
|
+
const { activeFieldName, values, viewportHeight } = options;
|
|
92
|
+
return getFirstPartyScrollTop({
|
|
93
|
+
activeFieldName,
|
|
94
|
+
fieldHeights: ADD_FIELD_HEIGHTS,
|
|
95
|
+
visibleFieldNames: getVisibleAddFieldNames(values),
|
|
96
|
+
viewportHeight,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function sanitizeAddSubmitValues(values: AddFlowValues): Record<string, unknown> {
|
|
101
|
+
const visibleFields = new Set(getVisibleAddFieldNames(values));
|
|
102
|
+
const sanitized: Record<string, unknown> = {};
|
|
103
|
+
|
|
104
|
+
for (const fieldName of visibleFields) {
|
|
105
|
+
const value = values[fieldName];
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
if (trimmed.length > 0) {
|
|
109
|
+
sanitized[fieldName] = trimmed;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (value !== undefined && value !== null) {
|
|
115
|
+
sanitized[fieldName] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return sanitized;
|
|
120
|
+
}
|