@ontrails/trails 1.0.0-beta.19 → 1.0.0-beta.21

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.
@@ -4,6 +4,7 @@ import {
4
4
  readFileSync,
5
5
  realpathSync,
6
6
  statSync,
7
+ symlinkSync,
7
8
  } from 'node:fs';
8
9
  import {
9
10
  dirname,
@@ -17,7 +18,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
17
18
 
18
19
  import {
19
20
  deriveSafePath,
20
- InternalError,
21
21
  isTrailsError,
22
22
  PermissionError,
23
23
  Result,
@@ -28,6 +28,7 @@ import { findAppModule } from '@ontrails/cli';
28
28
 
29
29
  import {
30
30
  createLoadAppMirrorRootPath,
31
+ ensureLoadAppMirrorDirectory,
31
32
  LOAD_APP_MIRROR_ENTRY_PREFIX,
32
33
  LOAD_APP_MIRROR_PARENT_DIRNAME,
33
34
  removeLoadAppMirrorRootQuietly,
@@ -161,12 +162,19 @@ const trustBoundaryError = (reason: string): PermissionError =>
161
162
  `Refusing to load an app module outside the workspace trust boundary (${reason}). Use a workspace-relative module path, or pass trustedModulePath: true from trusted code.`
162
163
  );
163
164
 
164
- const toLoadAppError = (error: unknown): Error =>
165
- isTrailsError(error)
166
- ? error
167
- : new InternalError('Failed to load app module', {
168
- cause: error instanceof Error ? error : new Error(String(error)),
169
- });
165
+ const asError = (error: unknown): Error =>
166
+ error instanceof Error ? error : new Error(String(error));
167
+
168
+ const toLoadAppError = (error: unknown): Error => {
169
+ if (isTrailsError(error)) {
170
+ return error;
171
+ }
172
+ const cause = asError(error);
173
+ return new ValidationError(`Failed to load app module: ${cause.message}`, {
174
+ cause,
175
+ context: { detail: cause.message },
176
+ });
177
+ };
170
178
 
171
179
  const isPathInside = (root: string, target: string): boolean => {
172
180
  const candidate = relative(root, target);
@@ -260,19 +268,31 @@ const isLocalFilesystemImport = (importPath: string): boolean =>
260
268
  importPath.startsWith('/') ||
261
269
  importPath.startsWith('file:');
262
270
 
263
- const readPackageName = (packagePath: string): string | undefined => {
271
+ interface PackageJson {
272
+ readonly exports?: unknown;
273
+ readonly imports?: unknown;
274
+ readonly main?: unknown;
275
+ readonly module?: unknown;
276
+ readonly name?: unknown;
277
+ readonly type?: unknown;
278
+ readonly workspaces?: unknown;
279
+ }
280
+
281
+ const readPackageJson = (packagePath: string): PackageJson | undefined => {
264
282
  try {
265
- const parsed = JSON.parse(readFileSync(packagePath, 'utf8')) as
266
- | { readonly name?: unknown }
267
- | undefined;
268
- return typeof parsed?.name === 'string' && parsed.name.length > 0
269
- ? parsed.name
270
- : undefined;
283
+ return JSON.parse(readFileSync(packagePath, 'utf8')) as PackageJson;
271
284
  } catch {
272
285
  return undefined;
273
286
  }
274
287
  };
275
288
 
289
+ const readPackageName = (packagePath: string): string | undefined => {
290
+ const parsed = readPackageJson(packagePath);
291
+ return typeof parsed?.name === 'string' && parsed.name.length > 0
292
+ ? parsed.name
293
+ : undefined;
294
+ };
295
+
276
296
  const findNearestPackageName = (directoryPath: string): string | undefined => {
277
297
  let current = directoryPath;
278
298
  while (true) {
@@ -304,13 +324,6 @@ const isPackageLocalImport = (
304
324
  );
305
325
  };
306
326
 
307
- const shouldMirrorImportSpecifier = (
308
- importerPath: string,
309
- importPath: string
310
- ): boolean =>
311
- isLocalFilesystemImport(importPath) ||
312
- isPackageLocalImport(importerPath, importPath);
313
-
314
327
  const isScannableModule = (modulePath: string): boolean =>
315
328
  SCANNABLE_EXTENSIONS.has(extname(modulePath));
316
329
 
@@ -328,10 +341,313 @@ const resolveImportedModulePath = (
328
341
  );
329
342
  };
330
343
 
331
- const collectImportedModulePaths = (
344
+ const readDirectoryEntries = (directoryPath: string): readonly string[] => {
345
+ try {
346
+ return readdirSync(directoryPath);
347
+ } catch {
348
+ return [];
349
+ }
350
+ };
351
+
352
+ const safeStat = (
353
+ entryPath: string
354
+ ): ReturnType<typeof statSync> | undefined => {
355
+ try {
356
+ return statSync(entryPath);
357
+ } catch {
358
+ return undefined;
359
+ }
360
+ };
361
+
362
+ const readWorkspacePatterns = (cwd: string): readonly string[] => {
363
+ const rootPackage = readPackageJson(join(cwd, 'package.json'));
364
+ const workspaces = rootPackage?.workspaces;
365
+ if (Array.isArray(workspaces)) {
366
+ return workspaces.filter(
367
+ (pattern): pattern is string => typeof pattern === 'string'
368
+ );
369
+ }
370
+ if (
371
+ typeof workspaces === 'object' &&
372
+ workspaces !== null &&
373
+ 'packages' in workspaces &&
374
+ Array.isArray(workspaces.packages)
375
+ ) {
376
+ return workspaces.packages.filter(
377
+ (pattern): pattern is string => typeof pattern === 'string'
378
+ );
379
+ }
380
+ return [];
381
+ };
382
+
383
+ interface WorkspacePackage {
384
+ readonly name: string;
385
+ readonly packageJson: PackageJson;
386
+ readonly packageRoot: string;
387
+ }
388
+
389
+ const listWorkspacePackageRoots = (cwd: string): readonly string[] => {
390
+ const roots: string[] = [];
391
+ for (const pattern of readWorkspacePatterns(cwd)) {
392
+ if (pattern.endsWith('/*')) {
393
+ const baseDir = resolve(cwd, pattern.slice(0, -2));
394
+ for (const entry of readDirectoryEntries(baseDir)) {
395
+ const entryPath = join(baseDir, entry);
396
+ if (safeStat(entryPath)?.isDirectory()) {
397
+ roots.push(entryPath);
398
+ }
399
+ }
400
+ continue;
401
+ }
402
+
403
+ if (!pattern.includes('*')) {
404
+ roots.push(resolve(cwd, pattern));
405
+ }
406
+ }
407
+ return roots;
408
+ };
409
+
410
+ const readWorkspacePackages = (cwd: string): readonly WorkspacePackage[] => {
411
+ const packages: WorkspacePackage[] = [];
412
+ for (const packageRoot of listWorkspacePackageRoots(cwd)) {
413
+ const packageJson = readPackageJson(join(packageRoot, 'package.json'));
414
+ if (
415
+ packageJson !== undefined &&
416
+ typeof packageJson.name === 'string' &&
417
+ packageJson.name.length > 0
418
+ ) {
419
+ packages.push({
420
+ name: packageJson.name,
421
+ packageJson,
422
+ packageRoot,
423
+ });
424
+ }
425
+ }
426
+ return packages;
427
+ };
428
+
429
+ const parsePackageSpecifier = (
430
+ importPath: string
431
+ ): { packageName: string; subpath: string } | null => {
432
+ if (URL_SCHEME.test(importPath)) {
433
+ return null;
434
+ }
435
+ if (importPath.startsWith('.') || importPath.startsWith('#')) {
436
+ return null;
437
+ }
438
+ const segments = importPath.split('/');
439
+ const [firstSegment, secondSegment] = segments;
440
+ if (firstSegment?.startsWith('@')) {
441
+ const scope = firstSegment;
442
+ const name = secondSegment;
443
+ if (name === undefined) {
444
+ return null;
445
+ }
446
+ const packageName = `${scope}/${name}`;
447
+ const rest = segments.slice(2).join('/');
448
+ return { packageName, subpath: rest.length > 0 ? `./${rest}` : '.' };
449
+ }
450
+ const [name] = segments;
451
+ if (name === undefined || name.length === 0) {
452
+ return null;
453
+ }
454
+ const rest = segments.slice(1).join('/');
455
+ return { packageName: name, subpath: rest.length > 0 ? `./${rest}` : '.' };
456
+ };
457
+
458
+ const resolveConditionalExportTarget = (
459
+ target: unknown
460
+ ): string | undefined => {
461
+ if (typeof target === 'string') {
462
+ return target;
463
+ }
464
+ if (typeof target !== 'object' || target === null || Array.isArray(target)) {
465
+ return undefined;
466
+ }
467
+ const record = target as Record<string, unknown>;
468
+ return (
469
+ resolveConditionalExportTarget(record['import']) ??
470
+ resolveConditionalExportTarget(record['default']) ??
471
+ resolveConditionalExportTarget(record['bun']) ??
472
+ resolveConditionalExportTarget(record['node'])
473
+ );
474
+ };
475
+
476
+ const resolveExportTarget = (
477
+ packageJson: PackageJson,
478
+ subpath: string
479
+ ): string | undefined => {
480
+ const { exports } = packageJson;
481
+ if (exports === undefined) {
482
+ if (subpath === '.') {
483
+ if (typeof packageJson.module === 'string') {
484
+ return packageJson.module;
485
+ }
486
+ if (typeof packageJson.main === 'string') {
487
+ return packageJson.main;
488
+ }
489
+ return './src/index.ts';
490
+ }
491
+ return subpath;
492
+ }
493
+ if (typeof exports === 'string' || Array.isArray(exports)) {
494
+ return subpath === '.'
495
+ ? resolveConditionalExportTarget(exports)
496
+ : undefined;
497
+ }
498
+ if (typeof exports !== 'object' || exports === null) {
499
+ return undefined;
500
+ }
501
+ return resolveConditionalExportTarget(
502
+ (exports as Record<string, unknown>)[subpath]
503
+ );
504
+ };
505
+
506
+ interface WorkspacePackageResolution {
507
+ readonly modulePath: string;
508
+ readonly packageName: string;
509
+ readonly packageRoot: string;
510
+ }
511
+
512
+ const resolveWorkspacePackageImport = (
513
+ importPath: string,
514
+ cwd: string
515
+ ): WorkspacePackageResolution | null => {
516
+ const parsed = parsePackageSpecifier(importPath);
517
+ if (parsed === null) {
518
+ return null;
519
+ }
520
+ const workspacePackage = readWorkspacePackages(cwd).find(
521
+ (candidate) => candidate.name === parsed.packageName
522
+ );
523
+ if (workspacePackage === undefined) {
524
+ return null;
525
+ }
526
+ const target = resolveExportTarget(
527
+ workspacePackage.packageJson,
528
+ parsed.subpath
529
+ );
530
+ if (target === undefined || !target.startsWith('.')) {
531
+ return null;
532
+ }
533
+ const targetPath = deriveSafePath(workspacePackage.packageRoot, target);
534
+ if (targetPath.isErr()) {
535
+ return null;
536
+ }
537
+ return {
538
+ modulePath: resolveFilesystemModulePath(
539
+ ensureRealPathInsideCwd(targetPath.value, cwd),
540
+ workspacePackage.packageRoot
541
+ ),
542
+ packageName: workspacePackage.name,
543
+ packageRoot: workspacePackage.packageRoot,
544
+ };
545
+ };
546
+
547
+ const findPackageRootForName = (
548
+ directoryPath: string,
549
+ packageName: string
550
+ ): string | null => {
551
+ let current = directoryPath;
552
+ while (true) {
553
+ const packagePath = join(current, 'package.json');
554
+ if (readPackageName(packagePath) === packageName) {
555
+ return current;
556
+ }
557
+
558
+ const parent = dirname(current);
559
+ if (parent === current) {
560
+ return null;
561
+ }
562
+ current = parent;
563
+ }
564
+ };
565
+
566
+ interface ExternalPackageResolution {
567
+ readonly packageName: string;
568
+ readonly packageRoot: string;
569
+ }
570
+
571
+ const resolveExternalPackageImport = (
572
+ importerPath: string,
573
+ importPath: string
574
+ ): ExternalPackageResolution | null => {
575
+ const parsed = parsePackageSpecifier(importPath);
576
+ if (parsed === null) {
577
+ return null;
578
+ }
579
+ let resolved: string;
580
+ try {
581
+ resolved = import.meta.resolve(
582
+ importPath,
583
+ pathToFileURL(importerPath).href
584
+ );
585
+ } catch {
586
+ return null;
587
+ }
588
+ if (!resolved.startsWith('file:')) {
589
+ return null;
590
+ }
591
+ const packageRoot = findPackageRootForName(
592
+ dirname(fileURLToPath(resolved)),
593
+ parsed.packageName
594
+ );
595
+ return packageRoot === null
596
+ ? null
597
+ : { packageName: parsed.packageName, packageRoot };
598
+ };
599
+
600
+ type MirrorImportResolution =
601
+ | {
602
+ readonly kind: 'module';
603
+ readonly modulePath: string;
604
+ }
605
+ | {
606
+ readonly kind: 'external-package';
607
+ readonly packageName: string;
608
+ readonly packageRoot: string;
609
+ }
610
+ | {
611
+ readonly kind: 'workspace-package';
612
+ readonly modulePath: string;
613
+ readonly packageName: string;
614
+ readonly packageRoot: string;
615
+ };
616
+
617
+ const resolveMirrorImport = (
618
+ importerPath: string,
619
+ importPath: string,
620
+ cwd: string
621
+ ): MirrorImportResolution | null => {
622
+ if (
623
+ isLocalFilesystemImport(importPath) ||
624
+ isPackageLocalImport(importerPath, importPath)
625
+ ) {
626
+ return {
627
+ kind: 'module',
628
+ modulePath: resolveImportedModulePath(importerPath, importPath),
629
+ };
630
+ }
631
+
632
+ const workspacePackage = resolveWorkspacePackageImport(importPath, cwd);
633
+ if (workspacePackage !== null) {
634
+ return { kind: 'workspace-package', ...workspacePackage };
635
+ }
636
+
637
+ const externalPackage = resolveExternalPackageImport(
638
+ importerPath,
639
+ importPath
640
+ );
641
+ return externalPackage === null
642
+ ? null
643
+ : { kind: 'external-package', ...externalPackage };
644
+ };
645
+
646
+ const collectImportedModuleResolutions = (
332
647
  modulePath: string,
333
- source: string
334
- ): readonly string[] => {
648
+ source: string,
649
+ cwd: string
650
+ ): readonly MirrorImportResolution[] => {
335
651
  const extension = extname(modulePath);
336
652
  const loader = LOADER_BY_EXTENSION[extension];
337
653
  if (loader === undefined) {
@@ -341,8 +657,10 @@ const collectImportedModulePaths = (
341
657
  return getImportScanner(loader)
342
658
  .scanImports(source)
343
659
  .map((entry) => entry.path)
344
- .filter((importPath) => shouldMirrorImportSpecifier(modulePath, importPath))
345
- .map((importPath) => resolveImportedModulePath(modulePath, importPath));
660
+ .map((importPath) => resolveMirrorImport(modulePath, importPath, cwd))
661
+ .filter(
662
+ (resolution): resolution is MirrorImportResolution => resolution !== null
663
+ );
346
664
  };
347
665
 
348
666
  const copyFileToMirror = async (
@@ -398,24 +716,6 @@ const MIRROR_SKIP_DIRECTORIES = new Set([
398
716
  * root at runtime without pulling in package installs or nested mirror
399
717
  * artifacts.
400
718
  */
401
- const readDirectoryEntries = (directoryPath: string): readonly string[] => {
402
- try {
403
- return readdirSync(directoryPath);
404
- } catch {
405
- return [];
406
- }
407
- };
408
-
409
- const safeStat = (
410
- entryPath: string
411
- ): ReturnType<typeof statSync> | undefined => {
412
- try {
413
- return statSync(entryPath);
414
- } catch {
415
- return undefined;
416
- }
417
- };
418
-
419
719
  /**
420
720
  * Age threshold (ms) above which a mirror entry in `.trails-tmp/` is
421
721
  * considered stale and safe to remove opportunistically.
@@ -480,9 +780,11 @@ const freshMirrorRootPath = (cwd: string): string => {
480
780
  };
481
781
 
482
782
  interface MirrorWalkContext {
783
+ readonly cwd: string;
483
784
  readonly mirrorRoot: string;
484
785
  readonly copied: Set<string>;
485
786
  readonly visitedDirectories: Set<string>;
787
+ readonly linkedPackageNames: Set<string>;
486
788
  }
487
789
 
488
790
  type DirectoryEntryKind = 'directory' | 'file' | 'skip';
@@ -542,6 +844,67 @@ const copyNearestPackageJsonToMirror = async (
542
844
  }
543
845
  };
544
846
 
847
+ const packageLinkSegments = (packageName: string): readonly string[] =>
848
+ packageName.split('/').filter((segment) => segment.length > 0);
849
+
850
+ const createPackageMirrorLink = (
851
+ packageName: string,
852
+ targetRoot: string,
853
+ context: MirrorWalkContext
854
+ ): void => {
855
+ if (context.linkedPackageNames.has(packageName)) {
856
+ return;
857
+ }
858
+ const mirrorWorkspaceRoot = resolveLoadAppMirrorFilePath(
859
+ context.cwd,
860
+ context.mirrorRoot
861
+ );
862
+ if (mirrorWorkspaceRoot.isErr()) {
863
+ throw mirrorWorkspaceRoot.error;
864
+ }
865
+ const linkPath = join(
866
+ mirrorWorkspaceRoot.value,
867
+ 'node_modules',
868
+ ...packageLinkSegments(packageName)
869
+ );
870
+
871
+ const ensured = ensureLoadAppMirrorDirectory(
872
+ dirname(linkPath),
873
+ context.mirrorRoot
874
+ );
875
+ if (ensured.isErr()) {
876
+ throw ensured.error;
877
+ }
878
+
879
+ try {
880
+ symlinkSync(targetRoot, linkPath, 'dir');
881
+ } catch (error) {
882
+ if (
883
+ !(error instanceof Error) ||
884
+ !('code' in error) ||
885
+ error.code !== 'EEXIST'
886
+ ) {
887
+ throw error;
888
+ }
889
+ }
890
+ context.linkedPackageNames.add(packageName);
891
+ };
892
+
893
+ const createWorkspacePackageMirrorLink = (
894
+ packageName: string,
895
+ packageRoot: string,
896
+ context: MirrorWalkContext
897
+ ): void => {
898
+ const mirrorPackageRoot = resolveLoadAppMirrorFilePath(
899
+ packageRoot,
900
+ context.mirrorRoot
901
+ );
902
+ if (mirrorPackageRoot.isErr()) {
903
+ throw mirrorPackageRoot.error;
904
+ }
905
+ createPackageMirrorLink(packageName, mirrorPackageRoot.value, context);
906
+ };
907
+
545
908
  const mirrorImportedModule = async (
546
909
  modulePath: string,
547
910
  context: MirrorWalkContext
@@ -557,24 +920,47 @@ const mirrorImportedModule = async (
557
920
 
558
921
  const scanAndVisitLocalImports = async (
559
922
  modulePath: string,
923
+ context: MirrorWalkContext,
560
924
  visit: (path: string) => Promise<void>
561
925
  ): Promise<void> => {
562
926
  if (!isScannableModule(modulePath)) {
563
927
  return;
564
928
  }
565
929
  const source = await Bun.file(modulePath).text();
566
- for (const importedPath of collectImportedModulePaths(modulePath, source)) {
567
- await visit(importedPath);
930
+ for (const imported of collectImportedModuleResolutions(
931
+ modulePath,
932
+ source,
933
+ context.cwd
934
+ )) {
935
+ if (imported.kind === 'external-package') {
936
+ createPackageMirrorLink(
937
+ imported.packageName,
938
+ imported.packageRoot,
939
+ context
940
+ );
941
+ continue;
942
+ }
943
+ if (imported.kind === 'workspace-package') {
944
+ createWorkspacePackageMirrorLink(
945
+ imported.packageName,
946
+ imported.packageRoot,
947
+ context
948
+ );
949
+ }
950
+ await visit(imported.modulePath);
568
951
  }
569
952
  };
570
953
 
571
954
  const mirrorFreshImportGraph = async (
572
955
  entryPath: string,
956
+ cwd: string,
573
957
  mirrorRoot: string
574
958
  ): Promise<string> => {
575
959
  const scanned = new Set<string>();
576
960
  const context: MirrorWalkContext = {
577
961
  copied: new Set<string>(),
962
+ cwd,
963
+ linkedPackageNames: new Set<string>(),
578
964
  mirrorRoot,
579
965
  visitedDirectories: new Set<string>(),
580
966
  };
@@ -584,7 +970,7 @@ const mirrorFreshImportGraph = async (
584
970
  return;
585
971
  }
586
972
  scanned.add(modulePath);
587
- await scanAndVisitLocalImports(modulePath, visit);
973
+ await scanAndVisitLocalImports(modulePath, context, visit);
588
974
  await mirrorImportedModule(modulePath, context);
589
975
  };
590
976
 
@@ -620,9 +1006,14 @@ const prepareMirror = async (
620
1006
  absolutePath: string,
621
1007
  cwd: string
622
1008
  ): Promise<{ mirrorRoot: string; freshPath: string }> => {
623
- const mirrorRoot = freshMirrorRootPath(cwd);
1009
+ const resolvedCwd = resolve(cwd);
1010
+ const mirrorRoot = freshMirrorRootPath(resolvedCwd);
624
1011
  try {
625
- const freshPath = await mirrorFreshImportGraph(absolutePath, mirrorRoot);
1012
+ const freshPath = await mirrorFreshImportGraph(
1013
+ absolutePath,
1014
+ resolvedCwd,
1015
+ mirrorRoot
1016
+ );
626
1017
  return { freshPath, mirrorRoot };
627
1018
  } catch (error) {
628
1019
  removeLoadAppMirrorRootQuietly(mirrorRoot);
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `release.check` trail -- Branch-local release rule evaluation.
3
+ */
4
+
5
+ import { Result, trail, ValidationError } from '@ontrails/core';
6
+ import { z } from 'zod';
7
+
8
+ import { runReleaseCheck } from '../release/check.js';
9
+ import { resolveTrailRootDir } from './root-dir.js';
10
+
11
+ const releaseCheckInputSchema = z.object({
12
+ baseRef: z
13
+ .string()
14
+ .optional()
15
+ .describe('Base git ref for changed-file and contract fact comparison'),
16
+ changedFiles: z
17
+ .string()
18
+ .optional()
19
+ .describe('Path to a newline-delimited changed-file list'),
20
+ configPath: z.string().optional().describe('Path to trails.config.ts'),
21
+ releaseNone: z
22
+ .boolean()
23
+ .default(false)
24
+ .describe('Compatibility no-release override'),
25
+ rootDir: z.string().optional().describe('Workspace root directory'),
26
+ });
27
+
28
+ const contractReleaseFactAspectSchema = z.enum([
29
+ 'input',
30
+ 'output',
31
+ 'surfaces',
32
+ 'trail',
33
+ 'visibility',
34
+ ]);
35
+
36
+ const contractReleaseFactSchema = z.object({
37
+ aspect: contractReleaseFactAspectSchema,
38
+ baseHash: z.string().nullable(),
39
+ changedFiles: z.array(z.string()).readonly(),
40
+ currentHash: z.string().nullable(),
41
+ packageName: z.string().optional(),
42
+ path: z.string(),
43
+ trailId: z.string(),
44
+ workspacePath: z.string().optional(),
45
+ });
46
+
47
+ const releaseCheckOutputSchema = z.object({
48
+ affectedPackages: z.array(z.string()).readonly(),
49
+ changedChangesets: z.array(z.string()).readonly(),
50
+ configPath: z.string().optional(),
51
+ contractFacts: z.array(contractReleaseFactSchema).readonly(),
52
+ coveredPackages: z.array(z.string()).readonly(),
53
+ errors: z.array(z.string()).readonly(),
54
+ formatted: z.string(),
55
+ matchedRuleIds: z.array(z.string()).readonly(),
56
+ noReleaseOverride: z.boolean(),
57
+ passed: z.boolean(),
58
+ releaseNone: z.boolean(),
59
+ uncoveredContractFacts: z.array(contractReleaseFactSchema).readonly(),
60
+ versionRelease: z.boolean(),
61
+ });
62
+
63
+ export const releaseCheckTrail = trail('release.check', {
64
+ blaze: async (input, ctx) => {
65
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
66
+ if (rootDirResult.isErr()) {
67
+ return rootDirResult;
68
+ }
69
+
70
+ try {
71
+ return Result.ok(
72
+ await runReleaseCheck({
73
+ ...(input.baseRef === undefined ? {} : { baseRef: input.baseRef }),
74
+ ...(input.changedFiles === undefined
75
+ ? {}
76
+ : { changedFilesPath: input.changedFiles }),
77
+ ...(input.configPath === undefined
78
+ ? {}
79
+ : { configPath: input.configPath }),
80
+ env: ctx.env ?? {},
81
+ releaseNone: input.releaseNone,
82
+ repoRoot: rootDirResult.value,
83
+ })
84
+ );
85
+ } catch (error) {
86
+ return Result.err(
87
+ new ValidationError(
88
+ error instanceof Error ? error.message : String(error)
89
+ )
90
+ );
91
+ }
92
+ },
93
+ description: 'Check branch-local release rules',
94
+ examples: [
95
+ {
96
+ input: { baseRef: 'HEAD' },
97
+ name: 'Check release rules from the current HEAD',
98
+ },
99
+ ],
100
+ input: releaseCheckInputSchema,
101
+ intent: 'read',
102
+ output: releaseCheckOutputSchema,
103
+ permit: 'public',
104
+ });