@wp-typia/project-tools 0.22.9 → 0.23.0

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 (90) hide show
  1. package/dist/runtime/ai-artifacts.js +3 -4
  2. package/dist/runtime/ai-feature-artifacts.js +2 -4
  3. package/dist/runtime/cli-add-collision.d.ts +25 -0
  4. package/dist/runtime/cli-add-collision.js +76 -0
  5. package/dist/runtime/cli-add-filesystem.js +2 -15
  6. package/dist/runtime/cli-add-help.js +11 -2
  7. package/dist/runtime/cli-add-kind-ids.d.ts +1 -1
  8. package/dist/runtime/cli-add-kind-ids.js +3 -0
  9. package/dist/runtime/cli-add-types.d.ts +117 -0
  10. package/dist/runtime/cli-add-types.js +29 -1
  11. package/dist/runtime/cli-add-validation.d.ts +90 -1
  12. package/dist/runtime/cli-add-validation.js +304 -1
  13. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +74 -19
  14. package/dist/runtime/cli-add-workspace-admin-view-source.js +11 -2
  15. package/dist/runtime/cli-add-workspace-admin-view-templates.d.ts +20 -2
  16. package/dist/runtime/cli-add-workspace-admin-view-templates.js +359 -3
  17. package/dist/runtime/cli-add-workspace-admin-view-types.d.ts +21 -0
  18. package/dist/runtime/cli-add-workspace-admin-view-types.js +22 -0
  19. package/dist/runtime/cli-add-workspace-ai-anchors.js +121 -31
  20. package/dist/runtime/cli-add-workspace-contract-source-emitters.d.ts +15 -0
  21. package/dist/runtime/cli-add-workspace-contract-source-emitters.js +42 -0
  22. package/dist/runtime/cli-add-workspace-contract.d.ts +15 -0
  23. package/dist/runtime/cli-add-workspace-contract.js +65 -0
  24. package/dist/runtime/cli-add-workspace-integration-env.d.ts +24 -0
  25. package/dist/runtime/cli-add-workspace-integration-env.js +391 -0
  26. package/dist/runtime/cli-add-workspace-post-meta-anchors.d.ts +23 -0
  27. package/dist/runtime/cli-add-workspace-post-meta-anchors.js +244 -0
  28. package/dist/runtime/cli-add-workspace-post-meta-source-emitters.d.ts +63 -0
  29. package/dist/runtime/cli-add-workspace-post-meta-source-emitters.js +179 -0
  30. package/dist/runtime/cli-add-workspace-post-meta.d.ts +15 -0
  31. package/dist/runtime/cli-add-workspace-post-meta.js +107 -0
  32. package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +1 -0
  33. package/dist/runtime/cli-add-workspace-rest-anchors.js +285 -21
  34. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +90 -2
  35. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +302 -29
  36. package/dist/runtime/cli-add-workspace-rest.d.ts +15 -2
  37. package/dist/runtime/cli-add-workspace-rest.js +329 -21
  38. package/dist/runtime/cli-add-workspace.d.ts +15 -0
  39. package/dist/runtime/cli-add-workspace.js +15 -0
  40. package/dist/runtime/cli-add.d.ts +1 -1
  41. package/dist/runtime/cli-add.js +1 -1
  42. package/dist/runtime/cli-core.d.ts +2 -1
  43. package/dist/runtime/cli-core.js +2 -1
  44. package/dist/runtime/cli-doctor-environment.js +1 -3
  45. package/dist/runtime/cli-doctor-workspace-features.js +128 -10
  46. package/dist/runtime/cli-doctor-workspace-package.d.ts +25 -3
  47. package/dist/runtime/cli-doctor-workspace-package.js +35 -13
  48. package/dist/runtime/cli-doctor-workspace-shared.d.ts +2 -0
  49. package/dist/runtime/cli-doctor-workspace-shared.js +5 -0
  50. package/dist/runtime/cli-doctor-workspace.d.ts +1 -1
  51. package/dist/runtime/cli-doctor-workspace.js +16 -8
  52. package/dist/runtime/cli-doctor.js +1 -1
  53. package/dist/runtime/cli-help.js +7 -0
  54. package/dist/runtime/cli-init-templates.js +11 -1
  55. package/dist/runtime/contract-artifacts.d.ts +14 -0
  56. package/dist/runtime/contract-artifacts.js +15 -0
  57. package/dist/runtime/fs-async.d.ts +7 -0
  58. package/dist/runtime/fs-async.js +11 -2
  59. package/dist/runtime/index.d.ts +1 -1
  60. package/dist/runtime/index.js +1 -1
  61. package/dist/runtime/migration-maintenance-verify.js +3 -0
  62. package/dist/runtime/migration-render-generated.js +4 -0
  63. package/dist/runtime/package-versions.js +3 -7
  64. package/dist/runtime/rest-resource-artifacts.d.ts +57 -1
  65. package/dist/runtime/rest-resource-artifacts.js +97 -1
  66. package/dist/runtime/scaffold-repository-reference.js +3 -7
  67. package/dist/runtime/template-render.d.ts +1 -1
  68. package/dist/runtime/template-render.js +1 -1
  69. package/dist/runtime/template-source-cache-markers.d.ts +37 -0
  70. package/dist/runtime/template-source-cache-markers.js +125 -0
  71. package/dist/runtime/template-source-cache.d.ts +1 -4
  72. package/dist/runtime/template-source-cache.js +16 -122
  73. package/dist/runtime/template-source-external.d.ts +4 -2
  74. package/dist/runtime/template-source-external.js +4 -2
  75. package/dist/runtime/template-source-remote.d.ts +8 -4
  76. package/dist/runtime/template-source-remote.js +8 -4
  77. package/dist/runtime/typia-llm.js +3 -4
  78. package/dist/runtime/workspace-inventory-mutations.d.ts +24 -0
  79. package/dist/runtime/workspace-inventory-mutations.js +181 -0
  80. package/dist/runtime/workspace-inventory-parser.d.ts +53 -0
  81. package/dist/runtime/workspace-inventory-parser.js +632 -0
  82. package/dist/runtime/workspace-inventory-read.d.ts +51 -0
  83. package/dist/runtime/workspace-inventory-read.js +98 -0
  84. package/dist/runtime/workspace-inventory-templates.d.ts +50 -0
  85. package/dist/runtime/workspace-inventory-templates.js +268 -0
  86. package/dist/runtime/workspace-inventory-types.d.ts +220 -0
  87. package/dist/runtime/workspace-inventory-types.js +1 -0
  88. package/dist/runtime/workspace-inventory.d.ts +5 -252
  89. package/dist/runtime/workspace-inventory.js +4 -928
  90. package/package.json +2 -2
@@ -2,24 +2,13 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { promises as fsp } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getNodeErrorCode, pathExists } from './fs-async.js';
5
+ import { CACHE_MARKER_FILE, CACHE_PRUNE_MARKER_FILE, externalTemplateCacheMetadataMatches, formatExternalTemplateCacheEntryMarker, formatExternalTemplateCachePruneMarker, isExternalTemplateCacheEntryFreshForTtl, parseExternalTemplateCacheEntryMarker, parseExternalTemplateCachePruneMarker, } from './template-source-cache-markers.js';
5
6
  import { getExternalTemplateCacheNowMs, getExternalTemplateCacheRoot, isExternalTemplateCacheEnabled, resolveExternalTemplateCachePruneIntervalMs, resolveExternalTemplateCacheTtlMs, } from './template-source-cache-policy.js';
6
7
  export { EXTERNAL_TEMPLATE_CACHE_DIR_ENV, EXTERNAL_TEMPLATE_CACHE_ENV, EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV, EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, getExternalTemplateCacheRoot, isExternalTemplateCacheEnabled, } from './template-source-cache-policy.js';
7
- /**
8
- * Marker file written after a cache entry is fully populated.
9
- */
10
- const CACHE_MARKER_FILE = 'wp-typia-template-cache.json';
11
- /**
12
- * Marker file written after a full TTL prune scan completes.
13
- */
14
- const CACHE_PRUNE_MARKER_FILE = 'wp-typia-template-cache-prune.json';
15
8
  /**
16
9
  * Private directory mode used for cache roots and entries on POSIX platforms.
17
10
  */
18
11
  const PRIVATE_CACHE_DIRECTORY_MODE = 0o700;
19
- /**
20
- * Marker value used when URL-like metadata cannot be safely normalized.
21
- */
22
- const REDACTED_CACHE_METADATA_VALUE = '[redacted]';
23
12
  /**
24
13
  * Filesystem errors that mean another writer published the same cache entry.
25
14
  */
@@ -34,10 +23,6 @@ const CACHE_UNAVAILABLE_ERROR_CODES = new Set([
34
23
  'EPERM',
35
24
  'EROFS',
36
25
  ]);
37
- /**
38
- * Metadata fields that may contain credentialed or signed URLs.
39
- */
40
- const URL_LIKE_METADATA_KEY = /(url|uri|registry|tarball)/iu;
41
26
  /**
42
27
  * Cache namespaces must stay within one path segment under the cache root.
43
28
  */
@@ -126,28 +111,6 @@ async function ensurePrivateCacheDirectory(directory) {
126
111
  return false;
127
112
  }
128
113
  }
129
- function sanitizeCacheMetadataValue(key, value) {
130
- if (!URL_LIKE_METADATA_KEY.test(key)) {
131
- return value;
132
- }
133
- try {
134
- const url = new URL(value);
135
- url.username = '';
136
- url.password = '';
137
- url.search = '';
138
- url.hash = '';
139
- return url.toString();
140
- }
141
- catch {
142
- return REDACTED_CACHE_METADATA_VALUE;
143
- }
144
- }
145
- function sanitizeCacheMetadata(metadata) {
146
- return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [
147
- key,
148
- value === null ? null : sanitizeCacheMetadataValue(key, value),
149
- ]));
150
- }
151
114
  function resolveCacheNamespaceDir(cacheRoot, namespace) {
152
115
  if (namespace === '.' ||
153
116
  namespace === '..' ||
@@ -185,51 +148,14 @@ async function isReusableCacheEntry(entryDir, markerPath, sourceDir) {
185
148
  (await pathExists(markerPath)) &&
186
149
  (await isDirectoryPath(sourceDir)));
187
150
  }
188
- function parseCacheMarkerMetadata(markerText) {
189
- let marker;
190
- try {
191
- marker = JSON.parse(markerText);
192
- }
193
- catch {
194
- return null;
195
- }
196
- if (typeof marker !== 'object' || marker === null || Array.isArray(marker)) {
197
- return null;
198
- }
199
- const rawMetadata = marker.metadata;
200
- if (typeof rawMetadata !== 'object' ||
201
- rawMetadata === null ||
202
- Array.isArray(rawMetadata)) {
203
- return null;
204
- }
205
- const metadata = {};
206
- for (const [key, value] of Object.entries(rawMetadata)) {
207
- if (typeof value !== 'string' && value !== null) {
208
- return null;
209
- }
210
- metadata[key] = value;
211
- }
212
- const rawCreatedAt = marker.createdAt;
213
- const createdAtMs = typeof rawCreatedAt === 'string' ? Date.parse(rawCreatedAt) : 0;
214
- return {
215
- createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0,
216
- metadata,
217
- };
218
- }
219
151
  async function readCacheEntryMarker(markerPath) {
220
152
  try {
221
- return parseCacheMarkerMetadata(await fsp.readFile(markerPath, 'utf8'));
153
+ return parseExternalTemplateCacheEntryMarker(await fsp.readFile(markerPath, 'utf8'));
222
154
  }
223
155
  catch {
224
156
  return null;
225
157
  }
226
158
  }
227
- function cacheMetadataMatches(actual, expected) {
228
- return Object.entries(expected).every(([key, value]) => actual[key] === value);
229
- }
230
- function isCacheEntryFreshForTtl(createdAtMs, nowMs, ttlMs) {
231
- return ttlMs === null || createdAtMs >= nowMs - ttlMs;
232
- }
233
159
  async function getReusableCacheEntryMarker(entryDir, markerPath, sourceDir) {
234
160
  if (!(await isReusableCacheEntry(entryDir, markerPath, sourceDir))) {
235
161
  return null;
@@ -238,7 +164,8 @@ async function getReusableCacheEntryMarker(entryDir, markerPath, sourceDir) {
238
164
  }
239
165
  async function isReusableFreshCacheEntry(entryDir, markerPath, sourceDir, nowMs, ttlMs) {
240
166
  const marker = await getReusableCacheEntryMarker(entryDir, markerPath, sourceDir);
241
- return (marker !== null && isCacheEntryFreshForTtl(marker.createdAtMs, nowMs, ttlMs));
167
+ return (marker !== null &&
168
+ isExternalTemplateCacheEntryFreshForTtl(marker.createdAtMs, nowMs, ttlMs));
242
169
  }
243
170
  function isPathInsideDirectory(directory, candidatePath) {
244
171
  const relativePath = path.relative(directory, candidatePath);
@@ -261,39 +188,6 @@ async function removeCacheEntryWithinRoot(cacheRoot, entryDir) {
261
188
  function getCachePruneMarkerPath(cacheRoot) {
262
189
  return path.join(cacheRoot, CACHE_PRUNE_MARKER_FILE);
263
190
  }
264
- function parseCachePruneMarker(markerText) {
265
- let marker;
266
- try {
267
- marker = JSON.parse(markerText);
268
- }
269
- catch {
270
- return null;
271
- }
272
- if (typeof marker !== 'object' || marker === null || Array.isArray(marker)) {
273
- return null;
274
- }
275
- const rawPrunedAt = marker.prunedAt;
276
- const prunedAtMs = typeof rawPrunedAt === 'string' ? Date.parse(rawPrunedAt) : Number.NaN;
277
- const rawPruneIntervalMs = marker
278
- .pruneIntervalMs;
279
- const rawTtlMs = marker.ttlMs;
280
- if (typeof rawTtlMs !== 'number' || !Number.isFinite(rawTtlMs)) {
281
- return null;
282
- }
283
- if (!Number.isFinite(prunedAtMs)) {
284
- return null;
285
- }
286
- if (rawPruneIntervalMs !== null &&
287
- (typeof rawPruneIntervalMs !== 'number' ||
288
- !Number.isFinite(rawPruneIntervalMs))) {
289
- return null;
290
- }
291
- return {
292
- prunedAtMs,
293
- pruneIntervalMs: rawPruneIntervalMs ?? null,
294
- ttlMs: rawTtlMs,
295
- };
296
- }
297
191
  async function shouldSkipExternalTemplateCachePrune({ cacheRoot, force, nowMs, pruneIntervalMs, ttlMs, }) {
298
192
  if (force || pruneIntervalMs === null) {
299
193
  return false;
@@ -305,7 +199,7 @@ async function shouldSkipExternalTemplateCachePrune({ cacheRoot, force, nowMs, p
305
199
  catch {
306
200
  return false;
307
201
  }
308
- const marker = parseCachePruneMarker(markerText);
202
+ const marker = parseExternalTemplateCachePruneMarker(markerText);
309
203
  if (!marker ||
310
204
  marker.ttlMs !== ttlMs ||
311
205
  marker.pruneIntervalMs !== pruneIntervalMs) {
@@ -316,11 +210,11 @@ async function shouldSkipExternalTemplateCachePrune({ cacheRoot, force, nowMs, p
316
210
  }
317
211
  async function writeExternalTemplateCachePruneMarker({ cacheRoot, nowMs, pruneIntervalMs, ttlMs, }) {
318
212
  try {
319
- await fsp.writeFile(getCachePruneMarkerPath(cacheRoot), `${JSON.stringify({
320
- prunedAt: new Date(nowMs).toISOString(),
213
+ await fsp.writeFile(getCachePruneMarkerPath(cacheRoot), formatExternalTemplateCachePruneMarker({
214
+ nowMs,
321
215
  pruneIntervalMs,
322
216
  ttlMs,
323
- }, null, 2)}\n`, 'utf8');
217
+ }), 'utf8');
324
218
  }
325
219
  catch {
326
220
  // Prune markers are an optimization; failing to write one keeps TTL safe.
@@ -476,8 +370,8 @@ export async function findReusableExternalTemplateSourceCache(descriptor) {
476
370
  const sourceDir = path.join(entryDir, 'source');
477
371
  const marker = await getReusableCacheEntryMarker(entryDir, markerPath, sourceDir);
478
372
  if (!marker ||
479
- !isCacheEntryFreshForTtl(marker.createdAtMs, nowMs, ttlMs) ||
480
- !cacheMetadataMatches(marker.metadata, descriptor.metadata)) {
373
+ !isExternalTemplateCacheEntryFreshForTtl(marker.createdAtMs, nowMs, ttlMs) ||
374
+ !externalTemplateCacheMetadataMatches(marker.metadata, descriptor.metadata)) {
481
375
  continue;
482
376
  }
483
377
  if (!bestEntry || marker.createdAtMs > bestEntry.createdAtMs) {
@@ -523,7 +417,7 @@ export async function resolveExternalTemplateSourceCache(descriptor, populateSou
523
417
  await pruneExternalTemplateCache();
524
418
  const existingMarker = await getReusableCacheEntryMarker(entryDir, markerPath, sourceDir);
525
419
  if (existingMarker &&
526
- isCacheEntryFreshForTtl(existingMarker.createdAtMs, nowMs, ttlMs)) {
420
+ isExternalTemplateCacheEntryFreshForTtl(existingMarker.createdAtMs, nowMs, ttlMs)) {
527
421
  return {
528
422
  cacheHit: true,
529
423
  sourceDir,
@@ -550,12 +444,12 @@ export async function resolveExternalTemplateSourceCache(descriptor, populateSou
550
444
  populateFailed = true;
551
445
  throw error;
552
446
  }
553
- await fsp.writeFile(path.join(temporaryEntryDir, CACHE_MARKER_FILE), `${JSON.stringify({
554
- createdAt: new Date().toISOString(),
555
- key: cacheKey,
556
- metadata: sanitizeCacheMetadata(descriptor.metadata),
447
+ await fsp.writeFile(path.join(temporaryEntryDir, CACHE_MARKER_FILE), formatExternalTemplateCacheEntryMarker({
448
+ cacheKey,
449
+ createdAt: new Date(),
450
+ metadata: descriptor.metadata,
557
451
  namespace: descriptor.namespace,
558
- }, null, 2)}\n`, 'utf8');
452
+ }), 'utf8');
559
453
  await fsp.rename(temporaryEntryDir, entryDir);
560
454
  return {
561
455
  cacheHit: false,
@@ -7,8 +7,10 @@ export declare const EXTERNAL_TEMPLATE_TRUST_WARNING = "External template config
7
7
  /**
8
8
  * Search a source directory for the first supported external template entry.
9
9
  *
10
- * @deprecated Use `findExternalTemplateEntry()` from async template-source
11
- * paths. This synchronous helper remains only for compatibility callers.
10
+ * @deprecated Since 0.22.10. Use `findExternalTemplateEntry()` from async
11
+ * template-source paths. Removal target: not currently scheduled; this sync
12
+ * compatibility helper remains available until release notes announce a
13
+ * versioned target.
12
14
  *
13
15
  * @param sourceDir Directory that may contain an external template config entry.
14
16
  * @returns The first matching entry path, or null when no supported entry exists.
@@ -33,8 +33,10 @@ function resolveSourceSubpath(sourceDir, relativePath) {
33
33
  /**
34
34
  * Search a source directory for the first supported external template entry.
35
35
  *
36
- * @deprecated Use `findExternalTemplateEntry()` from async template-source
37
- * paths. This synchronous helper remains only for compatibility callers.
36
+ * @deprecated Since 0.22.10. Use `findExternalTemplateEntry()` from async
37
+ * template-source paths. Removal target: not currently scheduled; this sync
38
+ * compatibility helper remains available until release notes announce a
39
+ * versioned target.
38
40
  *
39
41
  * @param sourceDir Directory that may contain an external template config entry.
40
42
  * @returns The first matching entry path, or null when no supported entry exists.
@@ -2,8 +2,10 @@ import type { ResolvedTemplateSource, SeedSource, TemplateVariableContext } from
2
2
  /**
3
3
  * Read a remote block source and return its default block category.
4
4
  *
5
- * @deprecated Use `getDefaultCategoryAsync()` from async template-source paths.
6
- * This synchronous helper remains only for compatibility callers.
5
+ * @deprecated Since 0.22.10. Use `getDefaultCategoryAsync()` from async
6
+ * template-source paths. Removal target: not currently scheduled; this sync
7
+ * compatibility helper remains available until release notes announce a
8
+ * versioned target.
7
9
  *
8
10
  * @param sourceDir Block source directory that may contain a block.json file.
9
11
  * @returns The declared block category, or "widgets" when detection fails.
@@ -20,8 +22,10 @@ export declare function getDefaultCategoryAsync(sourceDir: string): Promise<stri
20
22
  * Read `wpTypia.projectType` from a rendered or source template package
21
23
  * manifest and return it when present.
22
24
  *
23
- * @deprecated Use `getTemplateProjectTypeAsync()` from async template-source
24
- * paths. This synchronous helper remains only for compatibility callers.
25
+ * @deprecated Since 0.22.10. Use `getTemplateProjectTypeAsync()` from async
26
+ * template-source paths. Removal target: not currently scheduled; this sync
27
+ * compatibility helper remains available until release notes announce a
28
+ * versioned target.
25
29
  */
26
30
  export declare function getTemplateProjectType(sourceDir: string): string | null;
27
31
  /**
@@ -59,8 +59,10 @@ async function readRemoteBlockJsonAsync(blockDir) {
59
59
  /**
60
60
  * Read a remote block source and return its default block category.
61
61
  *
62
- * @deprecated Use `getDefaultCategoryAsync()` from async template-source paths.
63
- * This synchronous helper remains only for compatibility callers.
62
+ * @deprecated Since 0.22.10. Use `getDefaultCategoryAsync()` from async
63
+ * template-source paths. Removal target: not currently scheduled; this sync
64
+ * compatibility helper remains available until release notes announce a
65
+ * versioned target.
64
66
  *
65
67
  * @param sourceDir Block source directory that may contain a block.json file.
66
68
  * @returns The declared block category, or "widgets" when detection fails.
@@ -143,8 +145,10 @@ async function readTemplatePackageJsonAsync(sourceDir) {
143
145
  * Read `wpTypia.projectType` from a rendered or source template package
144
146
  * manifest and return it when present.
145
147
  *
146
- * @deprecated Use `getTemplateProjectTypeAsync()` from async template-source
147
- * paths. This synchronous helper remains only for compatibility callers.
148
+ * @deprecated Since 0.22.10. Use `getTemplateProjectTypeAsync()` from async
149
+ * template-source paths. Removal target: not currently scheduled; this sync
150
+ * compatibility helper remains available until release notes announce a
151
+ * versioned target.
148
152
  */
149
153
  export function getTemplateProjectType(sourceDir) {
150
154
  const packageJsonEntry = readTemplatePackageJson(sourceDir);
@@ -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,181 @@
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
+ !hasExportedTypeDeclaration(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 hasExportedTypeDeclaration(source, interfaceName) {
20
+ return new RegExp(`export\\s+(?:interface|type)\\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 upsertInterfaceField(source, interfaceName, fieldName, fieldSource) {
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 lineEnding = start.endsWith("\r\n") ? "\r\n" : "\n";
74
+ const formattedFieldSource = `${fieldSource
75
+ .replace(/\r?\n$/u, "")
76
+ .split("\n")
77
+ .join(lineEnding)}${lineEnding}`;
78
+ const existingFieldPattern = new RegExp(`(^[ \\t]*${escapeRegex(fieldName)}\\??:\\s*[^;\\r\\n]+;?\\r?\\n?)`, "mu");
79
+ const existingFieldMatch = existingFieldPattern.exec(body);
80
+ if (existingFieldMatch?.[0]) {
81
+ if (existingFieldMatch[0].trim() === fieldSource.trim()) {
82
+ return match;
83
+ }
84
+ return `${start}${body.slice(0, existingFieldMatch.index)}${formattedFieldSource}${body.slice(existingFieldMatch.index + existingFieldMatch[0].length)}${end}`;
85
+ }
86
+ const memberPattern = /^[ \t]*([A-Za-z_$][\w$]*)\??:/gmu;
87
+ for (const member of body.matchAll(memberPattern)) {
88
+ const memberIndex = member.index;
89
+ const memberName = member[1];
90
+ if (memberIndex === undefined || !memberName) {
91
+ continue;
92
+ }
93
+ if (memberName.localeCompare(fieldName) > 0) {
94
+ return `${start}${body.slice(0, memberIndex)}${formattedFieldSource}${body.slice(memberIndex)}${end}`;
95
+ }
96
+ }
97
+ return `${start}${body}${body.length > 0 && !body.endsWith(lineEnding) ? lineEnding : ""}${formattedFieldSource}${end}`;
98
+ });
99
+ }
100
+ function normalizeInterfaceFieldBlock(source, interfaceName, fieldName, fieldSource, requiredFragments) {
101
+ const interfacePattern = new RegExp(`(export\\s+interface\\s+${escapeRegex(interfaceName)}\\s*\\{\\r?\\n)([\\s\\S]*?)(\\r?\\n\\})`, "u");
102
+ return source.replace(interfacePattern, (match, start, body, end) => {
103
+ const fieldPattern = new RegExp(`(^([ \\t]*)${escapeRegex(fieldName)}\\??:\\s*\\{[ \\t]*\\r?\\n)([\\s\\S]*?)(^\\2\\};\\r?\\n?)`, "mu");
104
+ const fieldMatch = fieldPattern.exec(body);
105
+ if (!fieldMatch) {
106
+ return match;
107
+ }
108
+ const existingFieldSource = fieldMatch[0];
109
+ if (requiredFragments.every((fragment) => existingFieldSource.includes(fragment))) {
110
+ return match;
111
+ }
112
+ const lineEnding = start.endsWith("\r\n") ? "\r\n" : "\n";
113
+ const formattedFieldSource = `${fieldSource
114
+ .replace(/\r?\n$/u, "")
115
+ .split("\n")
116
+ .join(lineEnding)}${lineEnding}`;
117
+ return `${start}${body.slice(0, fieldMatch.index)}${formattedFieldSource}${body.slice(fieldMatch.index + existingFieldSource.length)}${end}`;
118
+ });
119
+ }
120
+ /**
121
+ * Update `scripts/block-config.ts` source text with additional inventory entries.
122
+ *
123
+ * Missing inventory sections for variations, patterns, binding sources, REST
124
+ * resources, workflow abilities, AI features, editor plugins, block styles, and
125
+ * block transforms are created
126
+ * automatically before new entries are appended at their marker comments.
127
+ * When provided, `transformSource` runs before any entries are inserted.
128
+ *
129
+ * @param source Existing `scripts/block-config.ts` source.
130
+ * @param options Entry lists plus an optional source transformer.
131
+ * @returns Updated source text with all requested inventory entries appended.
132
+ */
133
+ export function updateWorkspaceInventorySource(source, options = {}) {
134
+ let nextSource = ensureWorkspaceInventorySections(source);
135
+ if (options.transformSource) {
136
+ nextSource = options.transformSource(nextSource);
137
+ }
138
+ nextSource = appendInventorySectionEntries(nextSource, options);
139
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceBindingSourceConfig", "attribute", "\tattribute?: string;");
140
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceBindingSourceConfig", "block", "\tblock?: string;");
141
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceAbilityConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD);
142
+ nextSource = normalizeInterfaceFieldBlock(nextSource, "WorkspaceAbilityConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD, ["optionalFeatureIds: string[];", "requiredFeatureIds: string[];"]);
143
+ nextSource = ensureInterfaceField(nextSource, "WorkspaceAiFeatureConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD);
144
+ nextSource = normalizeInterfaceFieldBlock(nextSource, "WorkspaceAiFeatureConfig", "compatibility", WORKSPACE_COMPATIBILITY_CONFIG_FIELD, ["optionalFeatureIds: string[];", "requiredFeatureIds: string[];"]);
145
+ for (const [fieldName, fieldSource] of [
146
+ ["auth", "\tauth?: 'authenticated' | 'public' | 'public-write-protected';"],
147
+ ["bodyTypeName", "\tbodyTypeName?: string;"],
148
+ ["controllerClass", "\tcontrollerClass?: string;"],
149
+ ["controllerExtends", "\tcontrollerExtends?: string;"],
150
+ ["dataFile", "\tdataFile?: string;"],
151
+ ["method", "\tmethod?: 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';"],
152
+ ["mode", "\tmode?: 'generated' | 'manual';"],
153
+ ["pathPattern", "\tpathPattern?: string;"],
154
+ ["permissionCallback", "\tpermissionCallback?: string;"],
155
+ ["phpFile", "\tphpFile?: string;"],
156
+ ["queryTypeName", "\tqueryTypeName?: string;"],
157
+ ["responseTypeName", "\tresponseTypeName?: string;"],
158
+ ["routePattern", "\troutePattern?: string;"],
159
+ ["secretFieldName", "\tsecretFieldName?: string;"],
160
+ ["secretStateFieldName", "\tsecretStateFieldName?: string;"],
161
+ ]) {
162
+ nextSource = upsertInterfaceField(nextSource, "WorkspaceRestResourceConfig", fieldName, fieldSource);
163
+ }
164
+ return nextSource;
165
+ }
166
+ /**
167
+ * Append new entries to the canonical workspace inventory file on disk.
168
+ *
169
+ * @param projectDir Workspace root directory.
170
+ * @param options Entry lists and optional source transform passed through to
171
+ * `updateWorkspaceInventorySource`.
172
+ * @returns Resolves once `scripts/block-config.ts` has been updated if needed.
173
+ */
174
+ export async function appendWorkspaceInventoryEntries(projectDir, options) {
175
+ const blockConfigPath = path.join(projectDir, "scripts", "block-config.ts");
176
+ const source = await readFile(blockConfigPath, "utf8");
177
+ const nextSource = updateWorkspaceInventorySource(source, options);
178
+ if (nextSource !== source) {
179
+ await writeFile(blockConfigPath, nextSource, "utf8");
180
+ }
181
+ }
@@ -0,0 +1,53 @@
1
+ import type { WorkspaceInventory, WorkspaceInventoryAppendOptionKey, WorkspaceInventoryEntriesKey, WorkspaceInventorySectionFlagKey } from "./workspace-inventory-types.js";
2
+ type InventoryEntryFieldValue = string | string[] | boolean | undefined;
3
+ type InventoryEntryFieldValidationContext = {
4
+ elementIndex: number;
5
+ entryName: string;
6
+ key: string;
7
+ };
8
+ type InventoryEntryFieldDescriptor = {
9
+ key: string;
10
+ kind?: "boolean" | "string" | "stringArray";
11
+ required?: boolean;
12
+ validate?: (value: InventoryEntryFieldValue, context: InventoryEntryFieldValidationContext) => void;
13
+ };
14
+ type InventoryEntryParserDescriptor = {
15
+ entryName: string;
16
+ fields: readonly InventoryEntryFieldDescriptor[];
17
+ };
18
+ export type InventorySectionDescriptor = {
19
+ /** Optional marker metadata used when appending generated entries. */
20
+ append?: {
21
+ marker: string;
22
+ optionKey: WorkspaceInventoryAppendOptionKey;
23
+ };
24
+ /** Optional exported interface that backs the inventory section entries. */
25
+ interface?: {
26
+ name: string;
27
+ section: string;
28
+ };
29
+ /** Optional parser metadata for descriptor-driven inventory reads. */
30
+ parse?: {
31
+ entriesKey: WorkspaceInventoryEntriesKey;
32
+ entry: InventoryEntryParserDescriptor;
33
+ exportName?: string;
34
+ hasSectionKey?: WorkspaceInventorySectionFlagKey;
35
+ required?: boolean;
36
+ };
37
+ /** Optional exported const array that stores the inventory section entries. */
38
+ value?: {
39
+ name: string;
40
+ section: string;
41
+ };
42
+ };
43
+ export declare const BLOCK_INVENTORY_SECTION: InventorySectionDescriptor;
44
+ export declare const INVENTORY_SECTIONS: readonly InventorySectionDescriptor[];
45
+ /**
46
+ * Parse workspace inventory entries from the source of `scripts/block-config.ts`.
47
+ *
48
+ * @param source Raw TypeScript source from `scripts/block-config.ts`.
49
+ * @returns Parsed inventory sections without the resolved `blockConfigPath`.
50
+ * @throws {Error} When `BLOCKS` is missing or any inventory entry is malformed.
51
+ */
52
+ export declare function parseWorkspaceInventorySource(source: string): Omit<WorkspaceInventory, "blockConfigPath">;
53
+ export {};