@wp-typia/project-tools 0.22.7 → 0.22.9

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.
Files changed (63) hide show
  1. package/dist/runtime/block-targets.d.ts +40 -0
  2. package/dist/runtime/block-targets.js +71 -0
  3. package/dist/runtime/built-in-block-artifact-types.js +2 -1
  4. package/dist/runtime/built-in-block-attribute-specs.js +2 -1
  5. package/dist/runtime/built-in-block-code-artifacts.js +2 -0
  6. package/dist/runtime/built-in-block-non-ts-family-artifacts.js +12 -9
  7. package/dist/runtime/built-in-block-non-ts-render-utils.js +2 -0
  8. package/dist/runtime/cli-add-block-config.js +2 -1
  9. package/dist/runtime/cli-add-block.js +16 -4
  10. package/dist/runtime/cli-add-types.d.ts +8 -0
  11. package/dist/runtime/cli-add-types.js +11 -0
  12. package/dist/runtime/cli-add-workspace-ability-scaffold.js +21 -15
  13. package/dist/runtime/cli-add-workspace-ability.js +2 -2
  14. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +17 -13
  15. package/dist/runtime/cli-add-workspace-admin-view.js +2 -2
  16. package/dist/runtime/cli-add-workspace-ai.js +2 -2
  17. package/dist/runtime/cli-add-workspace-assets.js +42 -48
  18. package/dist/runtime/cli-add-workspace-rest.js +2 -2
  19. package/dist/runtime/cli-add-workspace.js +6 -38
  20. package/dist/runtime/cli-add.d.ts +3 -2
  21. package/dist/runtime/cli-add.js +2 -2
  22. package/dist/runtime/cli-core.d.ts +4 -2
  23. package/dist/runtime/cli-core.js +3 -2
  24. package/dist/runtime/cli-diagnostics.js +6 -0
  25. package/dist/runtime/cli-doctor-workspace.js +2 -0
  26. package/dist/runtime/cli-scaffold.js +5 -4
  27. package/dist/runtime/create-template-validation.d.ts +10 -0
  28. package/dist/runtime/create-template-validation.js +95 -0
  29. package/dist/runtime/id-suggestions.d.ts +21 -0
  30. package/dist/runtime/id-suggestions.js +48 -0
  31. package/dist/runtime/index.d.ts +5 -2
  32. package/dist/runtime/index.js +3 -1
  33. package/dist/runtime/migration-maintenance-verify.js +2 -0
  34. package/dist/runtime/package-versions.js +15 -2
  35. package/dist/runtime/php-utils.js +66 -0
  36. package/dist/runtime/scaffold-answer-resolution.js +5 -108
  37. package/dist/runtime/scaffold-apply-utils.js +3 -2
  38. package/dist/runtime/scaffold-identifiers.js +4 -3
  39. package/dist/runtime/scaffold-template-assertions.d.ts +6 -0
  40. package/dist/runtime/scaffold-template-assertions.js +33 -0
  41. package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
  42. package/dist/runtime/scaffold-template-variable-groups.js +7 -0
  43. package/dist/runtime/scaffold.js +3 -3
  44. package/dist/runtime/string-case.d.ts +2 -4
  45. package/dist/runtime/string-case.js +13 -4
  46. package/dist/runtime/template-builtins.js +11 -8
  47. package/dist/runtime/template-layers.js +2 -2
  48. package/dist/runtime/template-source-cache-policy.d.ts +53 -0
  49. package/dist/runtime/template-source-cache-policy.js +135 -0
  50. package/dist/runtime/template-source-cache.d.ts +1 -45
  51. package/dist/runtime/template-source-cache.js +9 -152
  52. package/dist/runtime/template-source-external.d.ts +3 -0
  53. package/dist/runtime/template-source-external.js +5 -2
  54. package/dist/runtime/template-source-remote.d.ts +6 -0
  55. package/dist/runtime/template-source-remote.js +8 -2
  56. package/dist/runtime/template-source-seeds.d.ts +13 -0
  57. package/dist/runtime/template-source-seeds.js +36 -8
  58. package/dist/runtime/template-source.js +2 -2
  59. package/dist/runtime/ts-property-names.d.ts +11 -0
  60. package/dist/runtime/ts-property-names.js +16 -0
  61. package/dist/runtime/workspace-inventory.d.ts +33 -7
  62. package/dist/runtime/workspace-inventory.js +94 -41
  63. package/package.json +11 -1
@@ -1,15 +1,17 @@
1
- import fs from "node:fs";
2
1
  import { promises as fsp } from "node:fs";
3
2
  import path from "node:path";
4
3
  import { syncBlockMetadata, } from "@wp-typia/block-runtime/metadata-core";
5
4
  import ts from "typescript";
6
5
  import { resolveWorkspaceProject, } from "./workspace-project.js";
7
- import { readWorkspaceInventory, appendWorkspaceInventoryEntries, } from "./workspace-inventory.js";
6
+ import { appendWorkspaceInventoryEntries, readWorkspaceInventoryAsync, } from "./workspace-inventory.js";
8
7
  import { toPascalCase, toTitleCase } from "./string-case.js";
9
8
  import { findPhpFunctionRange, hasPhpFunctionDefinition, quotePhpString, replacePhpFunctionDefinition, } from "./php-utils.js";
10
9
  import { assertBindingSourceDoesNotExist, assertEditorPluginDoesNotExist, assertPatternDoesNotExist, assertValidEditorPluginSlot, assertValidGeneratedSlug, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, resolveWorkspaceBlock, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
11
10
  import { appendPhpSnippetBeforeClosingTag, insertPhpSnippetBeforeWorkspaceAnchors, } from "./cli-add-workspace-mutation.js";
11
+ import { resolveWorkspaceBlockTargetName } from "./block-targets.js";
12
+ import { pathExists, readOptionalUtf8File } from "./fs-async.js";
12
13
  import { normalizeOptionalCliString } from "./cli-validation.js";
14
+ import { getPropertyNameText } from "./ts-property-names.js";
13
15
  const PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
14
16
  const BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
15
17
  const BINDING_SOURCE_EDITOR_SCRIPT = "build/bindings/index.js";
@@ -48,26 +50,6 @@ function assertValidBindingAttributeName(attributeName) {
48
50
  }
49
51
  return trimmed;
50
52
  }
51
- function resolveBindingTargetBlockSlug(blockName, namespace) {
52
- const trimmed = blockName.trim();
53
- if (!trimmed) {
54
- throw new Error("`wp-typia add binding-source` requires --block <block-slug|namespace/block-slug> to include a value when --attribute is provided.");
55
- }
56
- const blockNameSegments = trimmed.split("/");
57
- if (blockNameSegments.length > 2) {
58
- throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format.`);
59
- }
60
- if (blockNameSegments.some((segment) => segment.trim() === "")) {
61
- throw new Error(`Binding target block "${trimmed}" must use <block-slug> or <namespace/block-slug> format without empty path segments.`);
62
- }
63
- const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2
64
- ? blockNameSegments
65
- : [undefined, blockNameSegments[0]];
66
- if (maybeNamespace && maybeNamespace !== namespace) {
67
- throw new Error(`Binding target block "${trimmed}" uses namespace "${maybeNamespace}". Expected "${namespace}".`);
68
- }
69
- return normalizeBlockSlug(maybeSlug ?? "");
70
- }
71
53
  function buildEditorPluginConfigEntry(editorPluginSlug, slot) {
72
54
  return [
73
55
  "\t{",
@@ -237,9 +219,15 @@ function resolveBindingTarget(options, namespace) {
237
219
  if (!hasBlock || !hasAttribute) {
238
220
  throw new Error("`wp-typia add binding-source` requires --block and --attribute to be provided together.");
239
221
  }
222
+ const targetBlock = resolveWorkspaceBlockTargetName(blockName ?? "", namespace, {
223
+ empty: () => "`wp-typia add binding-source` requires --block <block-slug|namespace/block-slug> to include a value when --attribute is provided.",
224
+ emptySegment: (input) => `Binding target block "${input}" must use <block-slug> or <namespace/block-slug> format without empty path segments.`,
225
+ invalidFormat: (input) => `Binding target block "${input}" must use <block-slug> or <namespace/block-slug> format.`,
226
+ namespaceMismatch: (input, actualNamespace, expectedNamespace) => `Binding target block "${input}" uses namespace "${actualNamespace}". Expected "${expectedNamespace}".`,
227
+ });
240
228
  return {
241
229
  attributeName: assertValidBindingAttributeName(attributeName ?? ""),
242
- blockSlug: resolveBindingTargetBlockSlug(blockName ?? "", namespace),
230
+ blockSlug: targetBlock.blockSlug,
243
231
  };
244
232
  }
245
233
  function formatBindingAttributeTypeMember(attributeName) {
@@ -266,12 +254,6 @@ function getInterfaceDeclaration(source, interfaceName) {
266
254
  visit(sourceFile);
267
255
  return declaration ? { declaration, sourceFile } : undefined;
268
256
  }
269
- function getPropertyNameText(name) {
270
- if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
271
- return name.text;
272
- }
273
- return undefined;
274
- }
275
257
  function interfaceHasAttributeMember(declaration, attributeName) {
276
258
  return declaration.members.some((member) => ts.isPropertySignature(member) &&
277
259
  member.name !== undefined &&
@@ -722,41 +704,53 @@ async function ensureEditorPluginWebpackAnchors(workspace) {
722
704
  return nextSource;
723
705
  });
724
706
  }
725
- function resolveBindingSourceRegistryPath(projectDir) {
707
+ async function resolveBindingSourceRegistryPath(projectDir) {
726
708
  const bindingsDir = path.join(projectDir, "src", "bindings");
727
- return [path.join(bindingsDir, "index.ts"), path.join(bindingsDir, "index.js")].find((candidatePath) => fs.existsSync(candidatePath)) ?? path.join(bindingsDir, "index.ts");
709
+ for (const candidatePath of [
710
+ path.join(bindingsDir, "index.ts"),
711
+ path.join(bindingsDir, "index.js"),
712
+ ]) {
713
+ if (await pathExists(candidatePath)) {
714
+ return candidatePath;
715
+ }
716
+ }
717
+ return path.join(bindingsDir, "index.ts");
728
718
  }
729
719
  async function writeBindingSourceRegistry(projectDir, bindingSourceSlug) {
730
720
  const bindingsDir = path.join(projectDir, "src", "bindings");
731
- const bindingsIndexPath = resolveBindingSourceRegistryPath(projectDir);
721
+ const bindingsIndexPath = await resolveBindingSourceRegistryPath(projectDir);
732
722
  await fsp.mkdir(bindingsDir, { recursive: true });
733
- const existingBindingSourceSlugs = fs
734
- .readdirSync(bindingsDir, { withFileTypes: true })
723
+ const existingBindingSourceSlugs = (await fsp.readdir(bindingsDir, { withFileTypes: true }))
735
724
  .filter((entry) => entry.isDirectory())
736
725
  .map((entry) => entry.name);
737
726
  const nextBindingSourceSlugs = Array.from(new Set([...existingBindingSourceSlugs, bindingSourceSlug])).sort();
738
727
  await fsp.writeFile(bindingsIndexPath, buildBindingSourceIndexSource(nextBindingSourceSlugs), "utf8");
739
728
  }
740
- function resolveEditorPluginRegistryPath(projectDir) {
729
+ async function resolveEditorPluginRegistryPath(projectDir) {
741
730
  const editorPluginsDir = path.join(projectDir, "src", "editor-plugins");
742
- return [
731
+ for (const candidatePath of [
743
732
  path.join(editorPluginsDir, "index.ts"),
744
733
  path.join(editorPluginsDir, "index.js"),
745
- ].find((candidatePath) => fs.existsSync(candidatePath)) ?? path.join(editorPluginsDir, "index.ts");
734
+ ]) {
735
+ if (await pathExists(candidatePath)) {
736
+ return candidatePath;
737
+ }
738
+ }
739
+ return path.join(editorPluginsDir, "index.ts");
746
740
  }
747
- function readEditorPluginRegistrySlugs(registryPath) {
748
- if (!fs.existsSync(registryPath)) {
741
+ async function readEditorPluginRegistrySlugs(registryPath) {
742
+ const source = await readOptionalUtf8File(registryPath);
743
+ if (source === null) {
749
744
  return [];
750
745
  }
751
- const source = fs.readFileSync(registryPath, "utf8");
752
746
  return Array.from(source.matchAll(/^\s*import\s+['"]\.\/([^/'"]+)(?:\/index(?:\.[cm]?[jt]sx?)?)?['"];?\s*$/gmu)).map((match) => match[1]);
753
747
  }
754
748
  async function writeEditorPluginRegistry(projectDir, editorPluginSlug) {
755
749
  const editorPluginsDir = path.join(projectDir, "src", "editor-plugins");
756
- const registryPath = resolveEditorPluginRegistryPath(projectDir);
750
+ const registryPath = await resolveEditorPluginRegistryPath(projectDir);
757
751
  await fsp.mkdir(editorPluginsDir, { recursive: true });
758
- const existingEditorPluginSlugs = readWorkspaceInventory(projectDir).editorPlugins.map((entry) => entry.slug);
759
- const existingRegistrySlugs = readEditorPluginRegistrySlugs(registryPath);
752
+ const existingEditorPluginSlugs = (await readWorkspaceInventoryAsync(projectDir)).editorPlugins.map((entry) => entry.slug);
753
+ const existingRegistrySlugs = await readEditorPluginRegistrySlugs(registryPath);
760
754
  const nextEditorPluginSlugs = Array.from(new Set([...existingEditorPluginSlugs, ...existingRegistrySlugs, editorPluginSlug])).sort();
761
755
  await fsp.writeFile(registryPath, buildEditorPluginRegistrySource(nextEditorPluginSlugs), "utf8");
762
756
  }
@@ -779,12 +773,12 @@ export async function runAddEditorPluginCommand({ cwd = process.cwd(), editorPlu
779
773
  const workspace = resolveWorkspaceProject(cwd);
780
774
  const editorPluginSlug = assertValidGeneratedSlug("Editor plugin name", normalizeBlockSlug(editorPluginName), "wp-typia add editor-plugin <name> [--slot <sidebar|document-setting-panel>]");
781
775
  const resolvedSlot = assertValidEditorPluginSlot(slot);
782
- const inventory = readWorkspaceInventory(workspace.projectDir);
776
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
783
777
  assertEditorPluginDoesNotExist(workspace.projectDir, editorPluginSlug, inventory);
784
778
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
785
779
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
786
780
  const buildScriptPath = path.join(workspace.projectDir, "scripts", "build-workspace.mjs");
787
- const editorPluginsIndexPath = resolveEditorPluginRegistryPath(workspace.projectDir);
781
+ const editorPluginsIndexPath = await resolveEditorPluginRegistryPath(workspace.projectDir);
788
782
  const webpackConfigPath = path.join(workspace.projectDir, "webpack.config.js");
789
783
  const editorPluginDir = path.join(workspace.projectDir, "src", "editor-plugins", editorPluginSlug);
790
784
  const entryFilePath = path.join(editorPluginDir, "index.tsx");
@@ -848,7 +842,7 @@ export async function runAddEditorPluginCommand({ cwd = process.cwd(), editorPlu
848
842
  export async function runAddPatternCommand({ cwd = process.cwd(), patternName, }) {
849
843
  const workspace = resolveWorkspaceProject(cwd);
850
844
  const patternSlug = assertValidGeneratedSlug("Pattern name", normalizeBlockSlug(patternName), "wp-typia add pattern <name>");
851
- const inventory = readWorkspaceInventory(workspace.projectDir);
845
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
852
846
  assertPatternDoesNotExist(workspace.projectDir, patternSlug, inventory);
853
847
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
854
848
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
@@ -898,7 +892,7 @@ export async function runAddPatternCommand({ cwd = process.cwd(), patternName, }
898
892
  export async function runAddBindingSourceCommand({ attributeName, bindingSourceName, blockName, cwd = process.cwd(), }) {
899
893
  const workspace = resolveWorkspaceProject(cwd);
900
894
  const bindingSourceSlug = assertValidGeneratedSlug("Binding source name", normalizeBlockSlug(bindingSourceName), "wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>]");
901
- const inventory = readWorkspaceInventory(workspace.projectDir);
895
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
902
896
  assertBindingSourceDoesNotExist(workspace.projectDir, bindingSourceSlug, inventory);
903
897
  const target = resolveBindingTarget({
904
898
  attributeName,
@@ -907,7 +901,7 @@ export async function runAddBindingSourceCommand({ attributeName, bindingSourceN
907
901
  const targetBlock = target ? resolveWorkspaceBlock(inventory, target.blockSlug) : undefined;
908
902
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
909
903
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
910
- const bindingsIndexPath = resolveBindingSourceRegistryPath(workspace.projectDir);
904
+ const bindingsIndexPath = await resolveBindingSourceRegistryPath(workspace.projectDir);
911
905
  const bindingSourceDir = path.join(workspace.projectDir, "src", "bindings", bindingSourceSlug);
912
906
  const serverFilePath = path.join(bindingSourceDir, "server.php");
913
907
  const editorFilePath = path.join(bindingSourceDir, "editor.ts");
@@ -7,7 +7,7 @@ import { buildRestResourceApiSource, buildRestResourceConfigEntry, buildRestReso
7
7
  import { quotePhpString } from "./php-utils.js";
8
8
  import { syncRestResourceArtifacts } from "./rest-resource-artifacts.js";
9
9
  import { toPascalCase, toTitleCase } from "./string-case.js";
10
- import { appendWorkspaceInventoryEntries, readWorkspaceInventory, } from "./workspace-inventory.js";
10
+ import { appendWorkspaceInventoryEntries, readWorkspaceInventoryAsync, } from "./workspace-inventory.js";
11
11
  import { resolveWorkspaceProject } from "./workspace-project.js";
12
12
  function buildRestResourceRouteRegistrations(restResourceSlug, methods, functions) {
13
13
  const collectionRoutes = [];
@@ -434,7 +434,7 @@ export async function runAddRestResourceCommand({ cwd = process.cwd(), methods,
434
434
  const restResourceSlug = assertValidGeneratedSlug("REST resource name", normalizeBlockSlug(restResourceName), "wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create>]");
435
435
  const resolvedMethods = assertValidRestResourceMethods(methods);
436
436
  const resolvedNamespace = resolveRestResourceNamespace(workspace.workspace.namespace, namespace);
437
- const inventory = readWorkspaceInventory(workspace.projectDir);
437
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
438
438
  assertRestResourceDoesNotExist(workspace.projectDir, restResourceSlug, inventory);
439
439
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
440
440
  const bootstrapPath = getWorkspaceBootstrapPath(workspace);
@@ -1,8 +1,9 @@
1
1
  import { promises as fsp } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { pathExists } from "./fs-async.js";
4
+ import { assertFullBlockName, resolveWorkspaceTargetBlockName, } from "./block-targets.js";
4
5
  import { resolveWorkspaceProject } from "./workspace-project.js";
5
- import { appendWorkspaceInventoryEntries, readWorkspaceInventory } from "./workspace-inventory.js";
6
+ import { appendWorkspaceInventoryEntries, readWorkspaceInventoryAsync, } from "./workspace-inventory.js";
6
7
  import { toKebabCase, toSnakeCase, toTitleCase } from "./string-case.js";
7
8
  import { assertBlockStyleDoesNotExist, assertBlockTransformDoesNotExist, assertValidGeneratedSlug, assertValidHookAnchor, assertValidHookedBlockPosition, assertVariationDoesNotExist, getMutableBlockHooks, normalizeBlockSlug, patchFile, quoteTsString, readWorkspaceBlockJson, resolveWorkspaceBlock, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
8
9
  import { findExecutablePatternMatch, hasExecutablePattern, hasUncommentedPattern, maskTypeScriptCommentsAndLiterals, } from "./ts-source-masking.js";
@@ -19,7 +20,6 @@ const BLOCK_TRANSFORMS_IMPORT_PATTERN = /^\s*import\s*\{\s*applyWorkspaceBlockTr
19
20
  const BLOCK_TRANSFORMS_CALL_LINE = "applyWorkspaceBlockTransforms(registration.settings);";
20
21
  const BLOCK_TRANSFORMS_CALL_PATTERN = /applyWorkspaceBlockTransforms\s*\(\s*registration\s*\.\s*settings\s*\)\s*;?/u;
21
22
  const SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN = /registerScaffoldBlockType\s*\(\s*registration\s*\.\s*name\s*,\s*registration\s*\.\s*settings\s*\)\s*;?/u;
22
- const FULL_BLOCK_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/u;
23
23
  function isIdentifierBoundary(source, index) {
24
24
  if (index < 0 || index >= source.length) {
25
25
  return true;
@@ -407,38 +407,6 @@ async function writeBlockTransformRegistry(projectDir, blockSlug, transformSlug)
407
407
  const nextTransformSlugs = Array.from(new Set([...existingTransformSlugs, transformSlug])).sort();
408
408
  await fsp.writeFile(transformsIndexPath, buildBlockTransformIndexSource(nextTransformSlugs), "utf8");
409
409
  }
410
- function assertFullBlockName(blockName, flagName) {
411
- const trimmed = blockName.trim();
412
- if (!trimmed) {
413
- throw new Error(`\`${flagName}\` requires a block name.`);
414
- }
415
- if (!FULL_BLOCK_NAME_PATTERN.test(trimmed)) {
416
- throw new Error(`\`${flagName}\` must use <namespace/block-slug> format.`);
417
- }
418
- return trimmed;
419
- }
420
- function resolveWorkspaceTargetBlockName(blockName, namespace, flagName) {
421
- const trimmed = blockName.trim();
422
- if (!trimmed) {
423
- throw new Error(`\`${flagName}\` requires <block-slug|namespace/block-slug>.`);
424
- }
425
- const blockNameSegments = trimmed.split("/");
426
- if (blockNameSegments.length > 2 ||
427
- blockNameSegments.some((segment) => segment.trim() === "")) {
428
- throw new Error(`\`${flagName}\` must use <block-slug|namespace/block-slug> format.`);
429
- }
430
- const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2
431
- ? blockNameSegments
432
- : [undefined, blockNameSegments[0]];
433
- if (maybeNamespace && maybeNamespace !== namespace) {
434
- throw new Error(`\`${flagName}\` references namespace "${maybeNamespace}". Expected "${namespace}".`);
435
- }
436
- const blockSlug = normalizeBlockSlug(maybeSlug ?? "");
437
- return {
438
- blockName: `${namespace}/${blockSlug}`,
439
- blockSlug,
440
- };
441
- }
442
410
  /**
443
411
  * Re-export the DataViews admin screen scaffold workflow from the focused
444
412
  * admin-view runtime helper module.
@@ -484,7 +452,7 @@ export async function runAddVariationCommand({ blockName, cwd = process.cwd(), v
484
452
  const workspace = resolveWorkspaceProject(cwd);
485
453
  const blockSlug = normalizeBlockSlug(blockName);
486
454
  const variationSlug = assertValidGeneratedSlug("Variation name", normalizeBlockSlug(variationName), "wp-typia add variation <name> --block <block-slug>");
487
- const inventory = readWorkspaceInventory(workspace.projectDir);
455
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
488
456
  resolveWorkspaceBlock(inventory, blockSlug);
489
457
  assertVariationDoesNotExist(workspace.projectDir, blockSlug, variationSlug, inventory);
490
458
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
@@ -544,7 +512,7 @@ export async function runAddBlockStyleCommand({ blockName, cwd = process.cwd(),
544
512
  const workspace = resolveWorkspaceProject(cwd);
545
513
  const blockSlug = normalizeBlockSlug(blockName);
546
514
  const styleSlug = assertValidGeneratedSlug("Style name", normalizeBlockSlug(styleName), "wp-typia add style <name> --block <block-slug>");
547
- const inventory = readWorkspaceInventory(workspace.projectDir);
515
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
548
516
  resolveWorkspaceBlock(inventory, blockSlug);
549
517
  assertBlockStyleDoesNotExist(workspace.projectDir, blockSlug, styleSlug, inventory);
550
518
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
@@ -613,7 +581,7 @@ export async function runAddBlockTransformCommand({ cwd = process.cwd(), fromBlo
613
581
  const transformSlug = assertValidGeneratedSlug("Transform name", normalizeBlockSlug(transformName), "wp-typia add transform <name> --from <namespace/block> --to <block-slug|namespace/block-slug>");
614
582
  const resolvedFromBlockName = assertFullBlockName(fromBlockName, "--from");
615
583
  const target = resolveWorkspaceTargetBlockName(toBlockName, workspace.workspace.namespace, "--to");
616
- const inventory = readWorkspaceInventory(workspace.projectDir);
584
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
617
585
  resolveWorkspaceBlock(inventory, target.blockSlug);
618
586
  assertBlockTransformDoesNotExist(workspace.projectDir, target.blockSlug, transformSlug, inventory);
619
587
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
@@ -684,7 +652,7 @@ export async function runAddBlockTransformCommand({ cwd = process.cwd(), fromBlo
684
652
  export async function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd = process.cwd(), position, }) {
685
653
  const workspace = resolveWorkspaceProject(cwd);
686
654
  const blockSlug = normalizeBlockSlug(blockName);
687
- const inventory = readWorkspaceInventory(workspace.projectDir);
655
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
688
656
  resolveWorkspaceBlock(inventory, blockSlug);
689
657
  const resolvedAnchorBlockName = assertValidHookAnchor(anchorBlockName);
690
658
  const resolvedPosition = assertValidHookedBlockPosition(position);
@@ -7,8 +7,9 @@
7
7
  * - `cli-add-block` for built-in block scaffolding
8
8
  * - `cli-add-workspace` for workspace mutation commands
9
9
  */
10
- export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, } from "./cli-add-shared.js";
10
+ export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
11
11
  export type { AddBlockTemplateId, AddKindId, EditorPluginSlotId, } from "./cli-add-shared.js";
12
12
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
13
13
  export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
14
- export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
14
+ export { getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, } from "./workspace-inventory.js";
15
+ export type { WorkspaceBlockSelectOption } from "./workspace-inventory.js";
@@ -7,7 +7,7 @@
7
7
  * - `cli-add-block` for built-in block scaffolding
8
8
  * - `cli-add-workspace` for workspace mutation commands
9
9
  */
10
- export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, } from "./cli-add-shared.js";
10
+ export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
11
11
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
12
12
  export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
- export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
13
+ export { getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, } from "./workspace-inventory.js";
@@ -11,7 +11,8 @@
11
11
  * `runAddAdminViewCommand` for DataViews-backed admin screen scaffolds,
12
12
  * `runAddAbilityCommand` for typed workflow ability scaffolds,
13
13
  * and `HOOKED_BLOCK_POSITION_IDS`,
14
- * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
14
+ * `getWorkspaceBlockSelectOptions`, `getWorkspaceBlockSelectOptionsAsync`, and
15
+ * `seedWorkspaceMigrationProject` for
15
16
  * explicit `wp-typia add` flows,
16
17
  * `runAddAiFeatureCommand` for server-owned WordPress AI feature scaffolds,
17
18
  * `runAddRestResourceCommand` for plugin-level REST resource scaffolds,
@@ -31,11 +32,12 @@
31
32
  export { getDoctorChecks, runDoctor, type DoctorCheck } from "./cli-doctor.js";
32
33
  export { createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODE_METADATA, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getCliDiagnosticCodeMetadata, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
33
34
  export type { CliDiagnosticCode, CliDiagnosticCodeError, CliDiagnosticMessage, } from "./cli-diagnostics.js";
34
- export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
35
+ export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
35
36
  export { COMPOUND_INNER_BLOCKS_PRESET_IDS, getCompoundInnerBlocksPresetDefinition, } from "./compound-inner-blocks.js";
36
37
  export type { CompoundInnerBlocksPresetId } from "./compound-inner-blocks.js";
37
38
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
38
39
  export type { EditorPluginSlotId } from "./cli-add.js";
40
+ export type { WorkspaceBlockSelectOption } from "./workspace-inventory.js";
39
41
  export type { HookedBlockPositionId } from "./hooked-blocks.js";
40
42
  export { formatHelpText } from "./cli-help.js";
41
43
  export { getNextSteps, getOptionalOnboarding, runScaffoldFlow, } from "./cli-scaffold.js";
@@ -11,7 +11,8 @@
11
11
  * `runAddAdminViewCommand` for DataViews-backed admin screen scaffolds,
12
12
  * `runAddAbilityCommand` for typed workflow ability scaffolds,
13
13
  * and `HOOKED_BLOCK_POSITION_IDS`,
14
- * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
14
+ * `getWorkspaceBlockSelectOptions`, `getWorkspaceBlockSelectOptionsAsync`, and
15
+ * `seedWorkspaceMigrationProject` for
15
16
  * explicit `wp-typia add` flows,
16
17
  * `runAddAiFeatureCommand` for server-owned WordPress AI feature scaffolds,
17
18
  * `runAddRestResourceCommand` for plugin-level REST resource scaffolds,
@@ -30,7 +31,7 @@
30
31
  */
31
32
  export { getDoctorChecks, runDoctor } from "./cli-doctor.js";
32
33
  export { createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODE_METADATA, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getCliDiagnosticCodeMetadata, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
33
- export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
34
+ export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, runAddAdminViewCommand, runAddAbilityCommand, runAddBindingSourceCommand, runAddAiFeatureCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
34
35
  export { COMPOUND_INNER_BLOCKS_PRESET_IDS, getCompoundInnerBlocksPresetDefinition, } from "./compound-inner-blocks.js";
35
36
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
36
37
  export { formatHelpText } from "./cli-help.js";
@@ -226,6 +226,12 @@ export function createCliDiagnosticCodeError(code, message, options) {
226
226
  error.code = code;
227
227
  return error;
228
228
  }
229
+ /**
230
+ * Compatibility-only fallback for legacy or third-party errors that have not
231
+ * yet been tagged by their throw site. New user-facing failures should pass an
232
+ * explicit code through `createCliDiagnosticCodeError()` or
233
+ * `createCliCommandError({ code })` instead of relying on message matching.
234
+ */
229
235
  function inferCliDiagnosticCode(options) {
230
236
  const inheritedCode = readCliDiagnosticCode(options.error);
231
237
  if (inheritedCode) {
@@ -70,6 +70,8 @@ export function getWorkspaceDoctorChecks(cwd) {
70
70
  }
71
71
  checks.push(getWorkspacePackageMetadataCheck(workspace, workspacePackageJson));
72
72
  try {
73
+ // Doctor checks expose a synchronous API so callers can collect a stable
74
+ // snapshot without mixing async inventory reads into check aggregation.
73
75
  const inventory = readWorkspaceInventory(workspace.projectDir);
74
76
  checks.push(createDoctorCheck("Workspace inventory", "pass", formatWorkspaceInventorySummary(inventory)));
75
77
  checks.push(...getWorkspaceBlockDoctorChecks(workspace, inventory));
@@ -1,14 +1,15 @@
1
- import fs from "node:fs";
2
1
  import { promises as fsp } from "node:fs";
3
2
  import path from "node:path";
4
3
  import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDataStorageMode, isPersistencePolicy, resolvePackageManagerId, resolveTemplateId, scaffoldProject, } from "./scaffold.js";
5
4
  import { parseAlternateRenderTargets } from "./alternate-render-targets.js";
6
5
  import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
6
+ import { isCompoundPersistenceEnabled } from "./scaffold-template-variable-groups.js";
7
7
  import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
8
8
  import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
9
9
  import { createManagedTempRoot } from "./temp-roots.js";
10
10
  import { getOptionalOnboardingNote, getOptionalOnboardingShortNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
11
11
  import { formatNonEmptyTargetDirectoryError } from "./scaffold-bootstrap.js";
12
+ import { pathExists } from "./fs-async.js";
12
13
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
13
14
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
14
15
  import { assertBuiltInTemplateVariantAllowed, resolveLocalCliPathOption, normalizeOptionalCliString, } from "./cli-validation.js";
@@ -31,7 +32,7 @@ async function listRelativeProjectFiles(rootDir) {
31
32
  return relativeFiles.sort((left, right) => left.localeCompare(right));
32
33
  }
33
34
  async function assertDryRunTargetDirectoryReady(projectDir, allowExistingDir) {
34
- if (!fs.existsSync(projectDir) || allowExistingDir) {
35
+ if (!(await pathExists(projectDir)) || allowExistingDir) {
35
36
  return;
36
37
  }
37
38
  const entries = await fsp.readdir(projectDir);
@@ -424,7 +425,7 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
424
425
  let availableScripts;
425
426
  if (!dryRun) {
426
427
  try {
427
- const parsedPackageJson = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
428
+ const parsedPackageJson = JSON.parse(await fsp.readFile(path.join(projectDir, "package.json"), "utf8"));
428
429
  const scripts = parsedPackageJson.scripts &&
429
430
  typeof parsedPackageJson.scripts === "object" &&
430
431
  !Array.isArray(parsedPackageJson.scripts)
@@ -444,7 +445,7 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
444
445
  availableScripts,
445
446
  packageManager: resolvedPackageManager,
446
447
  templateId: resolvedTemplateId,
447
- compoundPersistenceEnabled: resolvedResult.result.variables.compoundPersistenceEnabled === "true",
448
+ compoundPersistenceEnabled: isCompoundPersistenceEnabled(resolvedResult.result.variables),
448
449
  }),
449
450
  plan: resolvedResult.plan,
450
451
  projectDir,
@@ -0,0 +1,10 @@
1
+ export declare const CREATE_TEMPLATE_SELECTION_HINT: string;
2
+ /**
3
+ * Validate an explicitly supplied create template id before entering the full
4
+ * scaffold flow.
5
+ *
6
+ * Built-in template ids and the workspace alias resolve immediately, common
7
+ * built-in typos keep suggestion diagnostics, and explicit external template
8
+ * locators remain deferred to the external template resolver.
9
+ */
10
+ export declare function validateExplicitCreateTemplateId(templateId: string): string;
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from "./cli-diagnostics.js";
3
+ import { OFFICIAL_WORKSPACE_TEMPLATE_ALIAS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from "./template-registry.js";
4
+ import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
5
+ import { parseNpmTemplateLocator } from "./template-source-locators.js";
6
+ import { suggestCloseId } from "./id-suggestions.js";
7
+ export const CREATE_TEMPLATE_SELECTION_HINT = `--template <${[
8
+ ...TEMPLATE_IDS,
9
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
10
+ ].join("|")}|./path|github:owner/repo/path[#ref]|npm-package>`;
11
+ const TEMPLATE_SUGGESTION_IDS = [
12
+ ...TEMPLATE_IDS,
13
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
14
+ ];
15
+ const USER_FACING_TEMPLATE_IDS = [
16
+ ...TEMPLATE_IDS,
17
+ OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
18
+ ];
19
+ function normalizeCreateTemplateSelection(templateId) {
20
+ return templateId === OFFICIAL_WORKSPACE_TEMPLATE_ALIAS
21
+ ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
22
+ : templateId;
23
+ }
24
+ function looksLikeWindowsAbsoluteTemplatePath(templateId) {
25
+ return /^[a-z]:[\\/]/iu.test(templateId) || /^\\\\[^\\]+\\[^\\]+/u.test(templateId);
26
+ }
27
+ function looksLikeExplicitNonNpmExternalTemplateLocator(templateId) {
28
+ return (path.isAbsolute(templateId) ||
29
+ looksLikeWindowsAbsoluteTemplatePath(templateId) ||
30
+ templateId.startsWith("./") ||
31
+ templateId.startsWith(".\\") ||
32
+ templateId.startsWith("../") ||
33
+ templateId.startsWith("..\\") ||
34
+ templateId.startsWith("@") ||
35
+ templateId.startsWith("github:") ||
36
+ templateId.includes("/") ||
37
+ templateId.includes("\\"));
38
+ }
39
+ function looksLikeExplicitCreateExternalTemplateLocator(templateId) {
40
+ return (looksLikeExplicitNonNpmExternalTemplateLocator(templateId) ||
41
+ parseNpmTemplateLocator(templateId) !== null);
42
+ }
43
+ function findMistypedBuiltInTemplateSuggestion(templateId) {
44
+ const normalizedTemplateId = templateId.trim().toLowerCase();
45
+ if (normalizedTemplateId.length === 0 ||
46
+ looksLikeExplicitNonNpmExternalTemplateLocator(normalizedTemplateId)) {
47
+ return null;
48
+ }
49
+ return suggestCloseId(normalizedTemplateId, TEMPLATE_SUGGESTION_IDS);
50
+ }
51
+ function getMistypedBuiltInTemplateMessage(templateId) {
52
+ const suggestion = findMistypedBuiltInTemplateSuggestion(templateId);
53
+ if (!suggestion) {
54
+ return null;
55
+ }
56
+ const suggestionDescription = suggestion === OFFICIAL_WORKSPACE_TEMPLATE_ALIAS
57
+ ? "official workspace scaffold"
58
+ : "built-in scaffold";
59
+ return `Unknown template "${templateId}". Did you mean "${suggestion}"? Use \`--template ${suggestion}\` for the ${suggestionDescription}, or pass a local path, \`github:owner/repo/path[#ref]\`, or an npm package spec for an external template.`;
60
+ }
61
+ function getUnknownTemplateMessage(templateId) {
62
+ return [
63
+ `Unknown template "${templateId}". Expected one of: ${USER_FACING_TEMPLATE_IDS.join(", ")}.`,
64
+ "Run `wp-typia templates list` to inspect available templates.",
65
+ "Pass an explicit external template locator such as `./path`, `github:owner/repo/path[#ref]`, or `@scope/template` for custom templates.",
66
+ ].join(" ");
67
+ }
68
+ /**
69
+ * Validate an explicitly supplied create template id before entering the full
70
+ * scaffold flow.
71
+ *
72
+ * Built-in template ids and the workspace alias resolve immediately, common
73
+ * built-in typos keep suggestion diagnostics, and explicit external template
74
+ * locators remain deferred to the external template resolver.
75
+ */
76
+ export function validateExplicitCreateTemplateId(templateId) {
77
+ const normalizedTemplateId = normalizeCreateTemplateSelection(templateId);
78
+ if (isRemovedBuiltInTemplateId(templateId)) {
79
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getRemovedBuiltInTemplateMessage(templateId));
80
+ }
81
+ if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
82
+ return normalizedTemplateId;
83
+ }
84
+ if (isBuiltInTemplateId(normalizedTemplateId)) {
85
+ return getTemplateById(normalizedTemplateId).id;
86
+ }
87
+ const mistypedBuiltInTemplateMessage = getMistypedBuiltInTemplateMessage(templateId);
88
+ if (mistypedBuiltInTemplateMessage) {
89
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, mistypedBuiltInTemplateMessage);
90
+ }
91
+ if (!looksLikeExplicitCreateExternalTemplateLocator(normalizedTemplateId)) {
92
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getUnknownTemplateMessage(templateId));
93
+ }
94
+ return normalizedTemplateId;
95
+ }
@@ -0,0 +1,21 @@
1
+ export interface SuggestCloseIdOptions {
2
+ /**
3
+ * Maximum edit distance accepted for a suggestion.
4
+ *
5
+ * Defaults to `2`, matching the create-template typo guard.
6
+ */
7
+ maxDistance?: number;
8
+ /**
9
+ * Normalizes user input and candidates before comparing them.
10
+ *
11
+ * Defaults to trimming and lowercasing for CLI id-like values.
12
+ */
13
+ normalize?: (value: string) => string;
14
+ }
15
+ /**
16
+ * Suggest the closest known id for a user-provided CLI value.
17
+ *
18
+ * This helper is intentionally generic so command-specific validation can keep
19
+ * its own wording and special-case handling while sharing typo thresholds.
20
+ */
21
+ export declare function suggestCloseId<const TCandidate extends string>(input: string, candidates: readonly TCandidate[], options?: SuggestCloseIdOptions): TCandidate | null;
@@ -0,0 +1,48 @@
1
+ function normalizeCloseId(value) {
2
+ return value.trim().toLowerCase();
3
+ }
4
+ function getEditDistance(left, right) {
5
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
6
+ const current = new Array(right.length + 1);
7
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
8
+ current[0] = leftIndex + 1;
9
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
10
+ const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
11
+ current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
12
+ }
13
+ for (let index = 0; index < current.length; index += 1) {
14
+ previous[index] = current[index];
15
+ }
16
+ }
17
+ return previous[right.length];
18
+ }
19
+ /**
20
+ * Suggest the closest known id for a user-provided CLI value.
21
+ *
22
+ * This helper is intentionally generic so command-specific validation can keep
23
+ * its own wording and special-case handling while sharing typo thresholds.
24
+ */
25
+ export function suggestCloseId(input, candidates, options = {}) {
26
+ const normalize = options.normalize ?? normalizeCloseId;
27
+ const normalizedInput = normalize(input);
28
+ if (normalizedInput.length === 0) {
29
+ return null;
30
+ }
31
+ const maxDistance = options.maxDistance ?? 2;
32
+ if (maxDistance < 0) {
33
+ return null;
34
+ }
35
+ let bestCandidate = null;
36
+ for (const candidateId of candidates) {
37
+ const distance = getEditDistance(normalizedInput, normalize(candidateId));
38
+ if (bestCandidate === null || distance < bestCandidate.distance) {
39
+ bestCandidate = {
40
+ distance,
41
+ id: candidateId,
42
+ };
43
+ }
44
+ }
45
+ return bestCandidate && bestCandidate.distance <= maxDistance
46
+ ? bestCandidate.id
47
+ : null;
48
+ }