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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wp-typia",
3
- "version": "0.16.1",
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.0",
68
+ "@wp-typia/project-tools": "0.16.2",
69
69
  "better-result": "^2.7.0",
70
- "react": "19.2.0",
71
- "react-dom": "19.2.0",
70
+ "react": "^19.2.5",
71
+ "react-dom": "^19.2.5",
72
72
  "zod": "4.3.6"
73
73
  },
74
74
  "devDependencies": {
@@ -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, getAddWorkspaceBlockOptions } from "../runtime-bridge";
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
  },
@@ -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 runDoctor(args.cwd);
20
+ await executeDoctorCommand(args.cwd);
21
21
  },
22
22
  name: "doctor",
23
23
  });
@@ -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
- parseMigrationArgs,
18
- tryResolveWorkspaceProject,
19
- runAddBindingSourceCommand,
20
- runAddBlockCommand,
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 function getAddWorkspaceBlockOptions(cwd: string) {
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
+ }