@wp-typia/project-tools 0.22.9 → 0.22.10

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.
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { buildWordPressAiArtifacts, } from './wordpress-ai.js';
4
+ import { getOptionalNodeErrorCode, isFileNotFoundError, } from './fs-async.js';
4
5
  export { buildWordPressAiArtifacts, buildWordPressAbilitiesDocument, projectWordPressAiSchema, } from './wordpress-ai.js';
5
6
  function normalizeGeneratedArtifactContent(content) {
6
7
  return content.replace(/\r\n?/g, '\n');
@@ -25,13 +26,11 @@ async function reconcileGeneratedAiArtifacts(artifacts, check) {
25
26
  }
26
27
  }
27
28
  catch (error) {
28
- const code = error && typeof error === 'object' && 'code' in error
29
- ? error.code
30
- : undefined;
31
- if (code === 'ENOENT') {
29
+ if (isFileNotFoundError(error)) {
32
30
  issues.push(`- ${artifact.filePath} (missing)`);
33
31
  continue;
34
32
  }
33
+ const code = getOptionalNodeErrorCode(error);
35
34
  issues.push(`- ${artifact.filePath} (unreadable: ${error instanceof Error ? error.message : code ?? 'unknown'})`);
36
35
  }
37
36
  }
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { defineEndpointManifest, syncEndpointClient, syncRestOpenApi, syncTypeSchemas, } from "@wp-typia/block-runtime/metadata-core";
4
4
  import { projectWordPressAiSchema } from "./ai-artifacts.js";
5
+ import { isFileNotFoundError } from "./fs-async.js";
5
6
  function normalizeGeneratedArtifactContent(content) {
6
7
  return content.replace(/\r\n?/g, "\n");
7
8
  }
@@ -21,10 +22,7 @@ async function reconcileGeneratedArtifact(options) {
21
22
  }
22
23
  }
23
24
  catch (error) {
24
- const code = error && typeof error === "object" && "code" in error
25
- ? error.code
26
- : undefined;
27
- if (code === "ENOENT") {
25
+ if (isFileNotFoundError(error)) {
28
26
  throw new Error(`Generated AI feature artifact is missing: ${options.label} (${options.filePath}).`);
29
27
  }
30
28
  throw error;
@@ -1,5 +1,6 @@
1
1
  import { promises as fsp } from "node:fs";
2
2
  import path from "node:path";
3
+ import { readOptionalUtf8File } from "./fs-async.js";
3
4
  /**
4
5
  * Resolve the primary PHP bootstrap file for an official workspace.
5
6
  *
@@ -24,15 +25,7 @@ export async function patchFile(filePath, transform) {
24
25
  * Read a file when it exists and otherwise return `null`.
25
26
  */
26
27
  export async function readOptionalFile(filePath) {
27
- try {
28
- return await fsp.readFile(filePath, "utf8");
29
- }
30
- catch (error) {
31
- if (isFileNotFoundError(error)) {
32
- return null;
33
- }
34
- throw error;
35
- }
28
+ return readOptionalUtf8File(filePath);
36
29
  }
37
30
  /**
38
31
  * Restore a file to its captured source, deleting it when the snapshot was `null`.
@@ -69,9 +62,3 @@ export async function rollbackWorkspaceMutation(snapshot) {
69
62
  await restoreOptionalFile(filePath, source);
70
63
  }
71
64
  }
72
- function isFileNotFoundError(error) {
73
- return (typeof error === "object" &&
74
- error !== null &&
75
- "code" in error &&
76
- error.code === "ENOENT");
77
- }
@@ -55,5 +55,7 @@ export const ADD_BLOCK_TEMPLATE_IDS = [
55
55
  * close-id threshold, otherwise `null`.
56
56
  */
57
57
  export function suggestAddBlockTemplateId(templateId) {
58
- return suggestCloseId(templateId, ADD_BLOCK_TEMPLATE_IDS);
58
+ return suggestCloseId(templateId, ADD_BLOCK_TEMPLATE_IDS, {
59
+ maxDistance: 3,
60
+ });
59
61
  }
@@ -90,6 +90,9 @@ export function resolveWorkspaceBootstrapPath(projectDir, packageName) {
90
90
  * @returns A passing or failing `DoctorCheck` describing any missing files.
91
91
  */
92
92
  export function checkExistingFiles(projectDir, label, filePaths) {
93
+ // Workspace category collectors remain synchronous pure mappers after the
94
+ // async inventory snapshot is loaded, so these small existence probes stay
95
+ // sync to preserve their current non-Promise APIs and output ordering.
93
96
  const missing = filePaths
94
97
  .filter((filePath) => typeof filePath === "string")
95
98
  .filter((filePath) => !fs.existsSync(path.join(projectDir, filePath)));
@@ -15,4 +15,4 @@ import type { DoctorCheck } from "./cli-doctor.js";
15
15
  * @param cwd Working directory expected to host an official workspace.
16
16
  * @returns Ordered workspace check rows ready for CLI rendering.
17
17
  */
18
- export declare function getWorkspaceDoctorChecks(cwd: string): DoctorCheck[];
18
+ export declare function getWorkspaceDoctorChecks(cwd: string): Promise<DoctorCheck[]>;
@@ -3,7 +3,7 @@ import { getWorkspaceBlockDoctorChecks, } from "./cli-doctor-workspace-blocks.js
3
3
  import { getWorkspaceFeatureDoctorChecks, } from "./cli-doctor-workspace-features.js";
4
4
  import { getMigrationWorkspaceHintCheck, getWorkspacePackageMetadataCheck, } from "./cli-doctor-workspace-package.js";
5
5
  import { createDoctorCheck, createDoctorScopeCheck, } from "./cli-doctor-workspace-shared.js";
6
- import { readWorkspaceInventory } from "./workspace-inventory.js";
6
+ import { readWorkspaceInventoryAsync, } from "./workspace-inventory.js";
7
7
  import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, tryResolveWorkspaceProject, } from "./workspace-project.js";
8
8
  function formatWorkspaceInventorySummary(inventory) {
9
9
  return [
@@ -36,11 +36,13 @@ function formatWorkspaceInventorySummary(inventory) {
36
36
  * @param cwd Working directory expected to host an official workspace.
37
37
  * @returns Ordered workspace check rows ready for CLI rendering.
38
38
  */
39
- export function getWorkspaceDoctorChecks(cwd) {
39
+ export async function getWorkspaceDoctorChecks(cwd) {
40
40
  const checks = [];
41
41
  let workspace = null;
42
42
  let invalidWorkspaceReason = null;
43
43
  try {
44
+ // Workspace discovery and package parsing intentionally stay synchronous
45
+ // because they share compatibility helpers with sync add/migration callers.
44
46
  invalidWorkspaceReason = getInvalidWorkspaceProjectReason(cwd);
45
47
  workspace = tryResolveWorkspaceProject(cwd);
46
48
  }
@@ -62,6 +64,9 @@ export function getWorkspaceDoctorChecks(cwd) {
62
64
  checks.push(createDoctorScopeCheck("pass", `Scope: full workspace diagnostics for ${workspace.workspace.namespace}. Environment readiness checks ran and workspace-scoped diagnostics are enabled for the package metadata, inventory, source-tree drift, and any configured migration hint rows below.`));
63
65
  let workspacePackageJson;
64
66
  try {
67
+ // Keep package metadata parsing sync until workspace-project exposes an
68
+ // async companion; the surrounding doctor orchestration already awaits
69
+ // async inventory reads below.
65
70
  workspacePackageJson = parseWorkspacePackageJson(workspace.projectDir);
66
71
  }
67
72
  catch (error) {
@@ -70,9 +75,7 @@ export function getWorkspaceDoctorChecks(cwd) {
70
75
  }
71
76
  checks.push(getWorkspacePackageMetadataCheck(workspace, workspacePackageJson));
72
77
  try {
73
- // Doctor checks expose a synchronous API so callers can collect a stable
74
- // snapshot without mixing async inventory reads into check aggregation.
75
- const inventory = readWorkspaceInventory(workspace.projectDir);
78
+ const inventory = await readWorkspaceInventoryAsync(workspace.projectDir);
76
79
  checks.push(createDoctorCheck("Workspace inventory", "pass", formatWorkspaceInventorySummary(inventory)));
77
80
  checks.push(...getWorkspaceBlockDoctorChecks(workspace, inventory));
78
81
  checks.push(...getWorkspaceBindingDoctorChecks(workspace, inventory));
@@ -15,7 +15,7 @@ import { getWorkspaceDoctorChecks } from "./cli-doctor-workspace.js";
15
15
  export async function getDoctorChecks(cwd) {
16
16
  return [
17
17
  ...(await getEnvironmentDoctorChecks(cwd)),
18
- ...getWorkspaceDoctorChecks(cwd),
18
+ ...(await getWorkspaceDoctorChecks(cwd)),
19
19
  ];
20
20
  }
21
21
  /**
@@ -19,6 +19,13 @@ export declare function readOptionalUtf8File(filePath: string): Promise<string |
19
19
  * @returns The string error code, or an empty string when unavailable.
20
20
  */
21
21
  export declare function getNodeErrorCode(error: unknown): string;
22
+ /**
23
+ * Extract a Node.js error code from an unknown thrown value when available.
24
+ *
25
+ * @param error Unknown error value.
26
+ * @returns The string error code, or `undefined` when unavailable.
27
+ */
28
+ export declare function getOptionalNodeErrorCode(error: unknown): string | undefined;
22
29
  /**
23
30
  * Return whether an unknown error represents a missing filesystem path.
24
31
  *
@@ -38,9 +38,18 @@ export async function readOptionalUtf8File(filePath) {
38
38
  * @returns The string error code, or an empty string when unavailable.
39
39
  */
40
40
  export function getNodeErrorCode(error) {
41
+ return getOptionalNodeErrorCode(error) ?? "";
42
+ }
43
+ /**
44
+ * Extract a Node.js error code from an unknown thrown value when available.
45
+ *
46
+ * @param error Unknown error value.
47
+ * @returns The string error code, or `undefined` when unavailable.
48
+ */
49
+ export function getOptionalNodeErrorCode(error) {
41
50
  return typeof error === "object" && error !== null && "code" in error
42
51
  ? String(error.code)
43
- : "";
52
+ : undefined;
44
53
  }
45
54
  /**
46
55
  * Return whether an unknown error represents a missing filesystem path.
@@ -49,5 +58,5 @@ export function getNodeErrorCode(error) {
49
58
  * @returns `true` when the error has Node.js code `ENOENT`.
50
59
  */
51
60
  export function isFileNotFoundError(error) {
52
- return getNodeErrorCode(error) === "ENOENT";
61
+ return getOptionalNodeErrorCode(error) === "ENOENT";
53
62
  }
@@ -59,6 +59,9 @@ export function verifyProjectMigrations(projectDir, { all = false, fromMigration
59
59
  return { verifiedVersions: targetVersions };
60
60
  }
61
61
  function recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck) {
62
+ // `migrate doctor` is still a synchronous maintenance command: it shares
63
+ // sync migration project loading, generated artifact comparisons, and
64
+ // execFileSync verification with the rest of migration maintenance.
62
65
  let invalidWorkspaceReason = null;
63
66
  let workspace;
64
67
  try {
@@ -217,6 +217,10 @@ export function renderPhpMigrationRegistryFile(state, entries) {
217
217
  return `<?php
218
218
  declare(strict_types=1);
219
219
 
220
+ if ( ! defined( 'ABSPATH' ) ) {
221
+ \texit;
222
+ }
223
+
220
224
  /**
221
225
  * Generated from advanced migration snapshots. Do not edit manually.
222
226
  */
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
+ import { getOptionalNodeErrorCode } from './fs-async.js';
4
5
  import { PROJECT_TOOLS_PACKAGE_ROOT } from './template-registry.js';
5
6
  const require = createRequire(import.meta.url);
6
7
  const DEFAULT_VERSION_RANGE = '^0.0.0';
@@ -21,11 +22,6 @@ export const DEFAULT_WORDPRESS_DATA_VERSION = '^9.28.0';
21
22
  export const DEFAULT_WORDPRESS_DATAVIEWS_VERSION = '^14.1.0';
22
23
  export const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = '^0.1.1';
23
24
  let cachedPackageVersions = null;
24
- function getErrorCode(error) {
25
- return typeof error === 'object' && error !== null && 'code' in error
26
- ? String(error.code)
27
- : undefined;
28
- }
29
25
  function normalizeVersionRange(value) {
30
26
  const trimmed = value?.trim();
31
27
  if (!trimmed) {
@@ -82,7 +78,7 @@ function resolvePackageManifestLocation(packageJsonPath) {
82
78
  };
83
79
  }
84
80
  catch (error) {
85
- if (getErrorCode(error) === 'ENOENT') {
81
+ if (getOptionalNodeErrorCode(error) === 'ENOENT') {
86
82
  return {
87
83
  cacheKey: `missing-file:${packageJsonPath}`,
88
84
  packageJsonPath: null,
@@ -114,7 +110,7 @@ function resolveInstalledPackageManifestLocation(packageName) {
114
110
  return resolvePackageManifestLocation(require.resolve(`${packageName}/package.json`));
115
111
  }
116
112
  catch (error) {
117
- if (getErrorCode(error) === 'MODULE_NOT_FOUND') {
113
+ if (getOptionalNodeErrorCode(error) === 'MODULE_NOT_FOUND') {
118
114
  return {
119
115
  cacheKey: `missing-module:${packageName}`,
120
116
  packageJsonPath: null,
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import path from "node:path";
4
+ import { getOptionalNodeErrorCode } from "./fs-async.js";
4
5
  import { PROJECT_TOOLS_PACKAGE_ROOT } from "./template-registry.js";
5
6
  const require = createRequire(import.meta.url);
6
7
  /**
@@ -8,17 +9,12 @@ const require = createRequire(import.meta.url);
8
9
  * can be resolved from the current runtime package manifests.
9
10
  */
10
11
  export const DEFAULT_SCAFFOLD_REPOSITORY_REFERENCE = "imjlk/wp-typia";
11
- function getErrorCode(error) {
12
- return typeof error === "object" && error !== null && "code" in error
13
- ? String(error.code)
14
- : undefined;
15
- }
16
12
  function readRepositoryPackageManifest(packageJsonPath) {
17
13
  try {
18
14
  return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
19
15
  }
20
16
  catch (error) {
21
- if (getErrorCode(error) === "ENOENT") {
17
+ if (getOptionalNodeErrorCode(error) === "ENOENT") {
22
18
  return null;
23
19
  }
24
20
  throw error;
@@ -29,7 +25,7 @@ function resolveInstalledPackageManifestPath(packageName) {
29
25
  return require.resolve(`${packageName}/package.json`);
30
26
  }
31
27
  catch (error) {
32
- if (getErrorCode(error) === "MODULE_NOT_FOUND") {
28
+ if (getOptionalNodeErrorCode(error) === "MODULE_NOT_FOUND") {
33
29
  return null;
34
30
  }
35
31
  throw error;
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { getOptionalNodeErrorCode, isFileNotFoundError, } from './fs-async.js';
3
4
  import { cloneJsonValue } from './json-utils.js';
4
5
  import { normalizeEndpointAuthDefinition, } from './schema-core.js';
5
6
  const TYPESCRIPT_RESERVED_WORDS = new Set([
@@ -433,13 +434,11 @@ async function reconcileGeneratedTypiaLlmArtifacts(artifacts, check) {
433
434
  }
434
435
  }
435
436
  catch (error) {
436
- const code = error && typeof error === 'object' && 'code' in error
437
- ? error.code
438
- : undefined;
439
- if (code === 'ENOENT') {
437
+ if (isFileNotFoundError(error)) {
440
438
  issues.push(`- ${artifact.filePath} (missing)`);
441
439
  continue;
442
440
  }
441
+ const code = getOptionalNodeErrorCode(error);
443
442
  issues.push(`- ${artifact.filePath} (unreadable: ${error instanceof Error ? error.message : code ?? 'unknown'})`);
444
443
  }
445
444
  }
@@ -0,0 +1,24 @@
1
+ import type { WorkspaceInventoryUpdateOptions } from "./workspace-inventory-types.js";
2
+ /**
3
+ * Update `scripts/block-config.ts` source text with additional inventory entries.
4
+ *
5
+ * Missing inventory sections for variations, patterns, binding sources, REST
6
+ * resources, workflow abilities, AI features, editor plugins, block styles, and
7
+ * block transforms are created
8
+ * automatically before new entries are appended at their marker comments.
9
+ * When provided, `transformSource` runs before any entries are inserted.
10
+ *
11
+ * @param source Existing `scripts/block-config.ts` source.
12
+ * @param options Entry lists plus an optional source transformer.
13
+ * @returns Updated source text with all requested inventory entries appended.
14
+ */
15
+ export declare function updateWorkspaceInventorySource(source: string, options?: WorkspaceInventoryUpdateOptions): string;
16
+ /**
17
+ * Append new entries to the canonical workspace inventory file on disk.
18
+ *
19
+ * @param projectDir Workspace root directory.
20
+ * @param options Entry lists and optional source transform passed through to
21
+ * `updateWorkspaceInventorySource`.
22
+ * @returns Resolves once `scripts/block-config.ts` has been updated if needed.
23
+ */
24
+ export declare function appendWorkspaceInventoryEntries(projectDir: string, options: Parameters<typeof updateWorkspaceInventorySource>[1]): Promise<void>;
@@ -0,0 +1,132 @@
1
+ import path from "node:path";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { escapeRegex } from "./php-utils.js";
4
+ import { BLOCK_INVENTORY_SECTION, INVENTORY_SECTIONS, } from "./workspace-inventory-parser.js";
5
+ import { WORKSPACE_COMPATIBILITY_CONFIG_FIELD } from "./workspace-inventory-templates.js";
6
+ function ensureWorkspaceInventorySections(source) {
7
+ let nextSource = source.trimEnd();
8
+ for (const section of INVENTORY_SECTIONS) {
9
+ if (section.interface &&
10
+ !hasExportedInterface(nextSource, section.interface.name)) {
11
+ nextSource += section.interface.section;
12
+ }
13
+ if (section.value && !hasExportedConst(nextSource, section.value.name)) {
14
+ nextSource += section.value.section;
15
+ }
16
+ }
17
+ return `${nextSource}\n`;
18
+ }
19
+ function hasExportedInterface(source, interfaceName) {
20
+ return new RegExp(`export\\s+interface\\s+${escapeRegex(interfaceName)}\\b`, "u").test(source);
21
+ }
22
+ function hasExportedConst(source, constName) {
23
+ return new RegExp(`export\\s+const\\s+${escapeRegex(constName)}\\b`, "u").test(source);
24
+ }
25
+ function appendEntriesAtMarker(source, marker, entries) {
26
+ if (entries.length === 0) {
27
+ return source;
28
+ }
29
+ if (!source.includes(marker)) {
30
+ throw new Error(`Workspace inventory marker "${marker}" is missing in scripts/block-config.ts.`);
31
+ }
32
+ const replacement = `${entries.join("\n")}\n${marker}`;
33
+ return source.replace(marker, () => replacement);
34
+ }
35
+ function appendInventorySectionEntries(source, options) {
36
+ let nextSource = source;
37
+ for (const section of [BLOCK_INVENTORY_SECTION, ...INVENTORY_SECTIONS]) {
38
+ if (!section.append) {
39
+ continue;
40
+ }
41
+ nextSource = appendEntriesAtMarker(nextSource, section.append.marker, options[section.append.optionKey] ?? []);
42
+ }
43
+ return nextSource;
44
+ }
45
+ function ensureInterfaceField(source, interfaceName, fieldName, fieldSource) {
46
+ const interfacePattern = new RegExp(`(export\\s+interface\\s+${escapeRegex(interfaceName)}\\s*\\{\\r?\\n)([\\s\\S]*?)(\\r?\\n\\})`, "u");
47
+ return source.replace(interfacePattern, (match, start, body, end) => {
48
+ if (new RegExp(`^[ \t]*${escapeRegex(fieldName)}\\??:`, "mu").test(body)) {
49
+ return match;
50
+ }
51
+ const lineEnding = start.endsWith("\r\n") ? "\r\n" : "\n";
52
+ const formattedFieldSource = `${fieldSource
53
+ .replace(/\r?\n$/u, "")
54
+ .split("\n")
55
+ .join(lineEnding)}${lineEnding}`;
56
+ const memberPattern = /^[ \t]*([A-Za-z_$][\w$]*)\??:/gmu;
57
+ for (const member of body.matchAll(memberPattern)) {
58
+ const memberIndex = member.index;
59
+ const memberName = member[1];
60
+ if (memberIndex === undefined || !memberName) {
61
+ continue;
62
+ }
63
+ if (memberName.localeCompare(fieldName) > 0) {
64
+ return `${start}${body.slice(0, memberIndex)}${formattedFieldSource}${body.slice(memberIndex)}${end}`;
65
+ }
66
+ }
67
+ return `${start}${body}${body.length > 0 && !body.endsWith(lineEnding) ? lineEnding : ""}${formattedFieldSource}${end}`;
68
+ });
69
+ }
70
+ function normalizeInterfaceFieldBlock(source, interfaceName, fieldName, fieldSource, requiredFragments) {
71
+ const interfacePattern = new RegExp(`(export\\s+interface\\s+${escapeRegex(interfaceName)}\\s*\\{\\r?\\n)([\\s\\S]*?)(\\r?\\n\\})`, "u");
72
+ return source.replace(interfacePattern, (match, start, body, end) => {
73
+ const fieldPattern = new RegExp(`(^([ \\t]*)${escapeRegex(fieldName)}\\??:\\s*\\{[ \\t]*\\r?\\n)([\\s\\S]*?)(^\\2\\};\\r?\\n?)`, "mu");
74
+ const fieldMatch = fieldPattern.exec(body);
75
+ if (!fieldMatch) {
76
+ return match;
77
+ }
78
+ const existingFieldSource = fieldMatch[0];
79
+ if (requiredFragments.every((fragment) => existingFieldSource.includes(fragment))) {
80
+ return match;
81
+ }
82
+ const lineEnding = start.endsWith("\r\n") ? "\r\n" : "\n";
83
+ const formattedFieldSource = `${fieldSource
84
+ .replace(/\r?\n$/u, "")
85
+ .split("\n")
86
+ .join(lineEnding)}${lineEnding}`;
87
+ return `${start}${body.slice(0, fieldMatch.index)}${formattedFieldSource}${body.slice(fieldMatch.index + existingFieldSource.length)}${end}`;
88
+ });
89
+ }
90
+ /**
91
+ * Update `scripts/block-config.ts` source text with additional inventory entries.
92
+ *
93
+ * Missing inventory sections for variations, patterns, binding sources, REST
94
+ * resources, workflow abilities, AI features, editor plugins, block styles, and
95
+ * block transforms are created
96
+ * automatically before new entries are appended at their marker comments.
97
+ * When provided, `transformSource` runs before any entries are inserted.
98
+ *
99
+ * @param source Existing `scripts/block-config.ts` source.
100
+ * @param options Entry lists plus an optional source transformer.
101
+ * @returns Updated source text with all requested inventory entries appended.
102
+ */
103
+ export function updateWorkspaceInventorySource(source, options = {}) {
104
+ let nextSource = ensureWorkspaceInventorySections(source);
105
+ if (options.transformSource) {
106
+ nextSource = options.transformSource(nextSource);
107
+ }
108
+ nextSource = appendInventorySectionEntries(nextSource, options);
109
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceBindingSourceConfig", "attribute", "\tattribute?: string;");
110
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceBindingSourceConfig", "block", "\tblock?: string;");
111
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceAbilityConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD);
112
+ nextSource = normalizeInterfaceFieldBlock(nextSource, "WorkspaceAbilityConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD, ["optionalFeatureIds: string[];", "requiredFeatureIds: string[];"]);
113
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceAiFeatureConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD);
114
+ nextSource = normalizeInterfaceFieldBlock(nextSource, "WorkspaceAiFeatureConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD, ["optionalFeatureIds: string[];", "requiredFeatureIds: string[];"]);
115
+ return nextSource;
116
+ }
117
+ /**
118
+ * Append new entries to the canonical workspace inventory file on disk.
119
+ *
120
+ * @param projectDir Workspace root directory.
121
+ * @param options Entry lists and optional source transform passed through to
122
+ * `updateWorkspaceInventorySource`.
123
+ * @returns Resolves once `scripts/block-config.ts` has been updated if needed.
124
+ */
125
+ export async function appendWorkspaceInventoryEntries(projectDir, options) {
126
+ const blockConfigPath = path.join(projectDir, "scripts", "block-config.ts");
127
+ const source = await readFile(blockConfigPath, "utf8");
128
+ const nextSource = updateWorkspaceInventorySource(source, options);
129
+ if (nextSource !== source) {
130
+ await writeFile(blockConfigPath, nextSource, "utf8");
131
+ }
132
+ }
@@ -0,0 +1,52 @@
1
+ import type { WorkspaceInventory, WorkspaceInventoryAppendOptionKey, WorkspaceInventoryEntriesKey, WorkspaceInventorySectionFlagKey } from "./workspace-inventory-types.js";
2
+ type InventoryEntryFieldValidationContext = {
3
+ elementIndex: number;
4
+ entryName: string;
5
+ key: string;
6
+ };
7
+ type InventoryEntryFieldDescriptor = {
8
+ key: string;
9
+ kind?: "string" | "stringArray";
10
+ required?: boolean;
11
+ validate?: (value: string | string[] | undefined, context: InventoryEntryFieldValidationContext) => void;
12
+ };
13
+ type InventoryEntryParserDescriptor = {
14
+ entryName: string;
15
+ fields: readonly InventoryEntryFieldDescriptor[];
16
+ };
17
+ export type InventorySectionDescriptor = {
18
+ /** Optional marker metadata used when appending generated entries. */
19
+ append?: {
20
+ marker: string;
21
+ optionKey: WorkspaceInventoryAppendOptionKey;
22
+ };
23
+ /** Optional exported interface that backs the inventory section entries. */
24
+ interface?: {
25
+ name: string;
26
+ section: string;
27
+ };
28
+ /** Optional parser metadata for descriptor-driven inventory reads. */
29
+ parse?: {
30
+ entriesKey: WorkspaceInventoryEntriesKey;
31
+ entry: InventoryEntryParserDescriptor;
32
+ exportName?: string;
33
+ hasSectionKey?: WorkspaceInventorySectionFlagKey;
34
+ required?: boolean;
35
+ };
36
+ /** Optional exported const array that stores the inventory section entries. */
37
+ value?: {
38
+ name: string;
39
+ section: string;
40
+ };
41
+ };
42
+ export declare const BLOCK_INVENTORY_SECTION: InventorySectionDescriptor;
43
+ export declare const INVENTORY_SECTIONS: readonly InventorySectionDescriptor[];
44
+ /**
45
+ * Parse workspace inventory entries from the source of `scripts/block-config.ts`.
46
+ *
47
+ * @param source Raw TypeScript source from `scripts/block-config.ts`.
48
+ * @returns Parsed inventory sections without the resolved `blockConfigPath`.
49
+ * @throws {Error} When `BLOCKS` is missing or any inventory entry is malformed.
50
+ */
51
+ export declare function parseWorkspaceInventorySource(source: string): Omit<WorkspaceInventory, "blockConfigPath">;
52
+ export {};