@ontrails/trails 1.0.0-beta.23 → 1.0.0-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/README.md +6 -5
- package/package.json +16 -14
- package/src/app.ts +42 -3
- package/src/cli.ts +17 -3
- package/src/local-state-io.ts +20 -0
- package/src/mcp-app.ts +10 -0
- package/src/mcp-options.ts +2 -3
- package/src/project-writes.ts +11 -11
- package/src/release/check.ts +41 -25
- package/src/release/index.ts +40 -0
- package/src/release/native-bun-registry.ts +282 -19
- package/src/release/pack-coherence.ts +457 -0
- package/src/release/policy.ts +1684 -0
- package/src/release/semver.ts +104 -0
- package/src/release/wayfinder-dogfood-smoke.ts +600 -66
- package/src/run-completions-install.ts +2 -2
- package/src/run-schema.ts +41 -0
- package/src/run-wayfind-outline.ts +166 -0
- package/src/trails/adapter-check.ts +1 -1
- package/src/trails/add-surface.ts +1 -1
- package/src/trails/add-verify.ts +3 -3
- package/src/trails/compile.ts +17 -25
- package/src/trails/create-scaffold.ts +14 -14
- package/src/trails/create.ts +5 -5
- package/src/trails/dev-support.ts +44 -24
- package/src/trails/doctor.ts +23 -2
- package/src/trails/draft-promote.ts +7 -7
- package/src/trails/guide.ts +15 -19
- package/src/trails/load-app.ts +58 -9
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/regrade.ts +72 -0
- package/src/trails/release-check.ts +1 -0
- package/src/trails/run-example.ts +21 -37
- package/src/trails/run-examples.ts +16 -32
- package/src/trails/run.ts +81 -57
- package/src/trails/survey.ts +76 -62
- package/src/trails/topo-output-schemas.ts +11 -0
- package/src/trails/topo-pin.ts +6 -20
- package/src/trails/topo-read-support.ts +91 -41
- package/src/trails/topo-reports.ts +4 -2
- package/src/trails/topo-store-support.ts +172 -39
- package/src/trails/topo-support.ts +1 -2
- package/src/trails/topo.ts +5 -19
- package/src/trails/validate.ts +9 -20
- package/src/trails/version-lifecycle-support.ts +10 -20
- package/src/trails/warden.ts +18 -0
- package/src/trails/wayfind.ts +1001 -0
package/src/trails/guide.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { withFreshOperatorApp } from './operator-context.js';
|
|
11
11
|
import { trailDetailOutput } from './topo-output-schemas.js';
|
|
12
12
|
import {
|
|
13
13
|
buildCurrentGuideEntries,
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
readSurfaceLayerNamesFromContext,
|
|
16
16
|
} from './topo-read-support.js';
|
|
17
17
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
18
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
19
18
|
|
|
20
19
|
// ---------------------------------------------------------------------------
|
|
21
20
|
// Types
|
|
@@ -28,6 +27,16 @@ interface GuideEntry {
|
|
|
28
27
|
readonly kind: 'trail';
|
|
29
28
|
}
|
|
30
29
|
|
|
30
|
+
type GuideTrailOutput =
|
|
31
|
+
| {
|
|
32
|
+
readonly entries: GuideEntry[];
|
|
33
|
+
readonly mode: 'list';
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
readonly detail: z.output<typeof trailDetailOutput>;
|
|
37
|
+
readonly mode: 'detail';
|
|
38
|
+
};
|
|
39
|
+
|
|
31
40
|
const guideTrailInputSchema = z.object({
|
|
32
41
|
module: z.string().optional().describe('Path to the app module'),
|
|
33
42
|
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
@@ -41,21 +50,11 @@ type GuideTrailInput = z.output<typeof guideTrailInputSchema>;
|
|
|
41
50
|
// ---------------------------------------------------------------------------
|
|
42
51
|
|
|
43
52
|
export const guideTrail = trail('guide', {
|
|
44
|
-
blaze: async (input: GuideTrailInput, ctx) =>
|
|
45
|
-
|
|
46
|
-
if (rootDirResult.isErr()) {
|
|
47
|
-
return rootDirResult;
|
|
48
|
-
}
|
|
49
|
-
const rootDir = rootDirResult.value;
|
|
50
|
-
const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
|
|
51
|
-
if (leaseResult.isErr()) {
|
|
52
|
-
return leaseResult;
|
|
53
|
-
}
|
|
54
|
-
const lease = leaseResult.value;
|
|
55
|
-
|
|
56
|
-
try {
|
|
53
|
+
blaze: async (input: GuideTrailInput, ctx) =>
|
|
54
|
+
withFreshOperatorApp<GuideTrailOutput>(input, ctx, ({ lease, rootDir }) => {
|
|
57
55
|
if (input.trailId) {
|
|
58
56
|
const detail = buildCurrentTopoDetail(lease.app, input.trailId, {
|
|
57
|
+
cliAliases: lease.cliAliases,
|
|
59
58
|
rootDir,
|
|
60
59
|
surfaceLayerNames: readSurfaceLayerNamesFromContext(ctx),
|
|
61
60
|
});
|
|
@@ -76,10 +75,7 @@ export const guideTrail = trail('guide', {
|
|
|
76
75
|
}) as GuideEntry[],
|
|
77
76
|
mode: 'list' as const,
|
|
78
77
|
});
|
|
79
|
-
}
|
|
80
|
-
lease.release();
|
|
81
|
-
}
|
|
82
|
-
},
|
|
78
|
+
}),
|
|
83
79
|
description: 'Runtime guidance for trails',
|
|
84
80
|
examples: [
|
|
85
81
|
{
|
package/src/trails/load-app.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
Result,
|
|
24
24
|
ValidationError,
|
|
25
25
|
} from '@ontrails/core';
|
|
26
|
-
import type { Topo } from '@ontrails/core';
|
|
26
|
+
import type { CliCommandAliasInput, Topo } from '@ontrails/core';
|
|
27
27
|
import { findAppModule } from '@ontrails/cli';
|
|
28
28
|
|
|
29
29
|
import {
|
|
@@ -1054,8 +1054,52 @@ const resolveLoadedTopo = (
|
|
|
1054
1054
|
return app;
|
|
1055
1055
|
};
|
|
1056
1056
|
|
|
1057
|
+
type LoadedCliAliases = Readonly<
|
|
1058
|
+
Record<string, readonly CliCommandAliasInput[]>
|
|
1059
|
+
>;
|
|
1060
|
+
|
|
1061
|
+
const isStringArray = (value: unknown): value is readonly string[] =>
|
|
1062
|
+
Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
1063
|
+
|
|
1064
|
+
const isCliAliasInput = (value: unknown): value is CliCommandAliasInput =>
|
|
1065
|
+
typeof value === 'string' || isStringArray(value);
|
|
1066
|
+
|
|
1067
|
+
const resolveLoadedCliAliases = (
|
|
1068
|
+
effectivePath: string,
|
|
1069
|
+
mod: Record<string, unknown>
|
|
1070
|
+
): LoadedCliAliases | undefined => {
|
|
1071
|
+
const value = mod['trailsCliAliases'] ?? mod['cliAliases'];
|
|
1072
|
+
if (value === undefined) {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
1076
|
+
throw new ValidationError(
|
|
1077
|
+
`CLI alias export in "${effectivePath}" must be a record from trail ID to alias list.`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const aliases = value as Record<string, unknown>;
|
|
1082
|
+
for (const [trailId, trailAliases] of Object.entries(aliases)) {
|
|
1083
|
+
if (!Array.isArray(trailAliases)) {
|
|
1084
|
+
throw new ValidationError(
|
|
1085
|
+
`CLI alias export for trail "${trailId}" in "${effectivePath}" must be an array.`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
for (const alias of trailAliases) {
|
|
1089
|
+
if (!isCliAliasInput(alias)) {
|
|
1090
|
+
throw new ValidationError(
|
|
1091
|
+
`CLI alias export for trail "${trailId}" in "${effectivePath}" must contain string aliases or string-array paths.`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return aliases as LoadedCliAliases;
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1057
1100
|
export interface FreshAppLease {
|
|
1058
1101
|
readonly app: Topo;
|
|
1102
|
+
readonly cliAliases?: LoadedCliAliases | undefined;
|
|
1059
1103
|
readonly mirrorRoot: string;
|
|
1060
1104
|
readonly release: () => void;
|
|
1061
1105
|
}
|
|
@@ -1071,14 +1115,18 @@ const noopRelease = (): void => undefined;
|
|
|
1071
1115
|
const createUrlSchemeLease = async (
|
|
1072
1116
|
absolutePath: string,
|
|
1073
1117
|
effectivePath: string
|
|
1074
|
-
): Promise<FreshAppLease> =>
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1118
|
+
): Promise<FreshAppLease> => {
|
|
1119
|
+
const mod = (await importWithCacheBust(absolutePath)) as Record<
|
|
1120
|
+
string,
|
|
1121
|
+
unknown
|
|
1122
|
+
>;
|
|
1123
|
+
return {
|
|
1124
|
+
app: resolveLoadedTopo(effectivePath, mod),
|
|
1125
|
+
cliAliases: resolveLoadedCliAliases(effectivePath, mod),
|
|
1126
|
+
mirrorRoot: absolutePath,
|
|
1127
|
+
release: noopRelease,
|
|
1128
|
+
};
|
|
1129
|
+
};
|
|
1082
1130
|
|
|
1083
1131
|
const createFilesystemLease = async (
|
|
1084
1132
|
absolutePath: string,
|
|
@@ -1096,6 +1144,7 @@ const createFilesystemLease = async (
|
|
|
1096
1144
|
|
|
1097
1145
|
return {
|
|
1098
1146
|
app: resolveLoadedTopo(effectivePath, mod),
|
|
1147
|
+
cliAliases: resolveLoadedCliAliases(effectivePath, mod),
|
|
1099
1148
|
mirrorRoot,
|
|
1100
1149
|
release,
|
|
1101
1150
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Result } from '@ontrails/core';
|
|
2
|
+
|
|
3
|
+
import { tryLoadFreshAppLease } from './load-app.js';
|
|
4
|
+
import type { FreshAppLease } from './load-app.js';
|
|
5
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
6
|
+
|
|
7
|
+
interface RootDirInput {
|
|
8
|
+
readonly rootDir?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FreshAppInput extends RootDirInput {
|
|
12
|
+
readonly module?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RootDirContext {
|
|
16
|
+
readonly cwd?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FreshAppContext {
|
|
20
|
+
readonly lease: FreshAppLease;
|
|
21
|
+
readonly rootDir: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const withOperatorRootDir = async <T>(
|
|
25
|
+
input: RootDirInput,
|
|
26
|
+
ctx: RootDirContext,
|
|
27
|
+
consume: (rootDir: string) => Result<T, Error> | Promise<Result<T, Error>>
|
|
28
|
+
): Promise<Result<T, Error>> => {
|
|
29
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
30
|
+
if (rootDirResult.isErr()) {
|
|
31
|
+
return rootDirResult;
|
|
32
|
+
}
|
|
33
|
+
return await consume(rootDirResult.value);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const withFreshAppLease = async <T>(
|
|
37
|
+
modulePath: string | undefined,
|
|
38
|
+
rootDir: string,
|
|
39
|
+
consume: (
|
|
40
|
+
lease: FreshAppLease
|
|
41
|
+
) => Result<T, Error> | Promise<Result<T, Error>>
|
|
42
|
+
): Promise<Result<T, Error>> => {
|
|
43
|
+
const leaseResult = await tryLoadFreshAppLease(modulePath, rootDir);
|
|
44
|
+
if (leaseResult.isErr()) {
|
|
45
|
+
return leaseResult;
|
|
46
|
+
}
|
|
47
|
+
const lease = leaseResult.value;
|
|
48
|
+
try {
|
|
49
|
+
return await consume(lease);
|
|
50
|
+
} finally {
|
|
51
|
+
lease.release();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const withFreshOperatorApp = async <T>(
|
|
56
|
+
input: FreshAppInput,
|
|
57
|
+
ctx: RootDirContext,
|
|
58
|
+
consume: (
|
|
59
|
+
context: FreshAppContext
|
|
60
|
+
) => Result<T, Error> | Promise<Result<T, Error>>
|
|
61
|
+
): Promise<Result<T, Error>> =>
|
|
62
|
+
withOperatorRootDir(input, ctx, (rootDir) =>
|
|
63
|
+
withFreshAppLease(input.module, rootDir, (lease) =>
|
|
64
|
+
consume({ lease, rootDir })
|
|
65
|
+
)
|
|
66
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `regrade` trail -- Run downstream migration checks and safe rewrites.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NotFoundError, Result, trail, validateOutput } from '@ontrails/core';
|
|
6
|
+
import type { Result as TrailsResult } from '@ontrails/core';
|
|
7
|
+
import {
|
|
8
|
+
regradeReportOutput,
|
|
9
|
+
runRegrade,
|
|
10
|
+
wardenTermRewriteClasses,
|
|
11
|
+
} from '@ontrails/regrade';
|
|
12
|
+
import type { RegradeReport } from '@ontrails/regrade';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
16
|
+
|
|
17
|
+
const regradeInputSchema = z.object({
|
|
18
|
+
apply: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe('Write safe rewrites to disk; dry-run report only by default'),
|
|
22
|
+
classIds: z
|
|
23
|
+
.array(z.string())
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Regrade class ids to run (defaults to all built-in classes)'),
|
|
26
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const regradeTrail = trail('regrade', {
|
|
30
|
+
blaze: (input, ctx) => {
|
|
31
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
32
|
+
if (rootDirResult.isErr()) {
|
|
33
|
+
return rootDirResult;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const reportResult: TrailsResult<RegradeReport | null, Error> = runRegrade({
|
|
37
|
+
apply: input.apply,
|
|
38
|
+
classes: wardenTermRewriteClasses,
|
|
39
|
+
root: rootDirResult.value,
|
|
40
|
+
...(input.classIds === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { selection: { classIds: input.classIds } }),
|
|
43
|
+
});
|
|
44
|
+
if (reportResult.isErr()) {
|
|
45
|
+
return reportResult;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const report = reportResult.value;
|
|
49
|
+
if (report === null) {
|
|
50
|
+
return Result.err(
|
|
51
|
+
new NotFoundError(
|
|
52
|
+
`Regrade root "${rootDirResult.value}" could not be read as a directory.`
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const validated: TrailsResult<
|
|
58
|
+
z.output<typeof regradeReportOutput>,
|
|
59
|
+
Error
|
|
60
|
+
> = validateOutput(regradeReportOutput, report);
|
|
61
|
+
if (validated.isErr()) {
|
|
62
|
+
return validated;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Result.ok(validated.value);
|
|
66
|
+
},
|
|
67
|
+
description: 'Run downstream migration checks and safe rewrites',
|
|
68
|
+
input: regradeInputSchema,
|
|
69
|
+
intent: 'write',
|
|
70
|
+
output: regradeReportOutput,
|
|
71
|
+
permit: 'public',
|
|
72
|
+
});
|
|
@@ -45,6 +45,7 @@ const contractReleaseFactSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
const releaseCheckOutputSchema = z.object({
|
|
48
|
+
activePackageChangesetsWithoutReleaseFacts: z.array(z.string()).readonly(),
|
|
48
49
|
affectedPackages: z.array(z.string()).readonly(),
|
|
49
50
|
changedChangesets: z.array(z.string()).readonly(),
|
|
50
51
|
configPath: z.string().optional(),
|
|
@@ -13,9 +13,8 @@ import {
|
|
|
13
13
|
import type { BasePermit, StructuredTrailExample, Topo } from '@ontrails/core';
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
|
|
17
17
|
import { resolveRunModulePath } from './run.js';
|
|
18
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
19
18
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
20
19
|
|
|
21
20
|
export const RUN_EXAMPLE_COMPARISON_KIND = 'example-comparison' as const;
|
|
@@ -355,7 +354,7 @@ const buildComparisonEnvelope = async (
|
|
|
355
354
|
): Promise<Result<RunExampleComparison, Error>> => {
|
|
356
355
|
const exampleResult = findExample(app, trailId, exampleName);
|
|
357
356
|
if (exampleResult.isErr()) {
|
|
358
|
-
return
|
|
357
|
+
return exampleResult;
|
|
359
358
|
}
|
|
360
359
|
const example = exampleResult.value;
|
|
361
360
|
const mode = determineMode(example);
|
|
@@ -440,42 +439,27 @@ type RunExampleTrailInput = z.output<typeof runExampleTrailInputSchema>;
|
|
|
440
439
|
|
|
441
440
|
export const runExampleTrail = trail('run.example', {
|
|
442
441
|
args: ['id', 'exampleName'],
|
|
443
|
-
blaze: async (input: RunExampleTrailInput, ctx) =>
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const rootDir = rootDirResult.value;
|
|
449
|
-
const moduleResolution = await resolveRunModulePath(
|
|
450
|
-
rootDir,
|
|
451
|
-
input.module,
|
|
452
|
-
input.id,
|
|
453
|
-
input.app
|
|
454
|
-
);
|
|
455
|
-
if (moduleResolution.isErr()) {
|
|
456
|
-
return moduleResolution;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const leaseResult = await tryLoadFreshAppLease(
|
|
460
|
-
moduleResolution.value,
|
|
461
|
-
rootDir
|
|
462
|
-
);
|
|
463
|
-
if (leaseResult.isErr()) {
|
|
464
|
-
return leaseResult;
|
|
465
|
-
}
|
|
466
|
-
const lease = leaseResult.value;
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
return await buildComparisonEnvelope(
|
|
470
|
-
lease.app,
|
|
442
|
+
blaze: async (input: RunExampleTrailInput, ctx) =>
|
|
443
|
+
withOperatorRootDir(input, ctx, async (rootDir) => {
|
|
444
|
+
const moduleResolution = await resolveRunModulePath(
|
|
445
|
+
rootDir,
|
|
446
|
+
input.module,
|
|
471
447
|
input.id,
|
|
472
|
-
input.
|
|
473
|
-
ctx.permit
|
|
448
|
+
input.app
|
|
474
449
|
);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
450
|
+
if (moduleResolution.isErr()) {
|
|
451
|
+
return moduleResolution;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return withFreshAppLease(moduleResolution.value, rootDir, (lease) =>
|
|
455
|
+
buildComparisonEnvelope(
|
|
456
|
+
lease.app,
|
|
457
|
+
input.id,
|
|
458
|
+
input.exampleName,
|
|
459
|
+
ctx.permit
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
}),
|
|
479
463
|
description: 'Run a named example on a trail and compare actual vs expected',
|
|
480
464
|
examples: [
|
|
481
465
|
{
|
|
@@ -11,9 +11,8 @@ import {
|
|
|
11
11
|
import type { StructuredTrailExample, Topo } from '@ontrails/core';
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
|
|
15
15
|
import { resolveRunModulePath } from './run.js';
|
|
16
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
17
16
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
18
17
|
|
|
19
18
|
export const RUN_EXAMPLES_LISTING_KIND = 'examples-listing' as const;
|
|
@@ -100,37 +99,22 @@ type RunExamplesTrailInput = z.output<typeof runExamplesTrailInputSchema>;
|
|
|
100
99
|
|
|
101
100
|
export const runExamplesTrail = trail('run.examples', {
|
|
102
101
|
args: ['id'],
|
|
103
|
-
blaze: async (input: RunExamplesTrailInput, ctx) =>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
115
|
-
if (moduleResolution.isErr()) {
|
|
116
|
-
return moduleResolution;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const leaseResult = await tryLoadFreshAppLease(
|
|
120
|
-
moduleResolution.value,
|
|
121
|
-
rootDir
|
|
122
|
-
);
|
|
123
|
-
if (leaseResult.isErr()) {
|
|
124
|
-
return leaseResult;
|
|
125
|
-
}
|
|
126
|
-
const lease = leaseResult.value;
|
|
102
|
+
blaze: async (input: RunExamplesTrailInput, ctx) =>
|
|
103
|
+
withOperatorRootDir(input, ctx, async (rootDir) => {
|
|
104
|
+
const moduleResolution = await resolveRunModulePath(
|
|
105
|
+
rootDir,
|
|
106
|
+
input.module,
|
|
107
|
+
input.id,
|
|
108
|
+
input.app
|
|
109
|
+
);
|
|
110
|
+
if (moduleResolution.isErr()) {
|
|
111
|
+
return moduleResolution;
|
|
112
|
+
}
|
|
127
113
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
},
|
|
114
|
+
return withFreshAppLease(moduleResolution.value, rootDir, (lease) =>
|
|
115
|
+
buildExamplesListing(lease.app, input.id)
|
|
116
|
+
);
|
|
117
|
+
}),
|
|
134
118
|
description: "List a trail's examples without executing it",
|
|
135
119
|
examples: [
|
|
136
120
|
{
|
package/src/trails/run.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
AmbiguousError,
|
|
34
34
|
NotFoundError,
|
|
35
35
|
Result,
|
|
36
|
+
ValidationError,
|
|
36
37
|
run,
|
|
37
38
|
trail,
|
|
38
39
|
} from '@ontrails/core';
|
|
@@ -49,8 +50,7 @@ import {
|
|
|
49
50
|
writeIsolatedExampleTextFile,
|
|
50
51
|
} from '../local-state-io.js';
|
|
51
52
|
|
|
52
|
-
import {
|
|
53
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
53
|
+
import { withFreshAppLease, withOperatorRootDir } from './operator-context.js';
|
|
54
54
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
55
55
|
|
|
56
56
|
export const INNER_TRAIL_RESULT_KIND = 'inner-trail-result' as const;
|
|
@@ -313,73 +313,97 @@ const buildAmbiguousExampleInput = (): {
|
|
|
313
313
|
return { id: 'shared.id', rootDir: root };
|
|
314
314
|
};
|
|
315
315
|
|
|
316
|
-
const runTrailInputSchema = z
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
316
|
+
const runTrailInputSchema = z
|
|
317
|
+
.object({
|
|
318
|
+
app: z
|
|
319
|
+
.string()
|
|
320
|
+
.optional()
|
|
321
|
+
.describe(
|
|
322
|
+
'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
|
|
323
|
+
),
|
|
324
|
+
id: z.string().describe('Trail ID to invoke'),
|
|
325
|
+
input: z
|
|
326
|
+
.unknown()
|
|
327
|
+
.optional()
|
|
328
|
+
.describe(
|
|
329
|
+
'Parsed input for the resolved trail; the CLI surface JSON.parses the inline argument before passing it through'
|
|
330
|
+
),
|
|
331
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
332
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
333
|
+
})
|
|
334
|
+
.catchall(z.unknown());
|
|
333
335
|
|
|
334
336
|
type RunTrailInput = z.output<typeof runTrailInputSchema>;
|
|
335
337
|
|
|
338
|
+
const RUN_TRAIL_CONTROL_KEYS = new Set([
|
|
339
|
+
'app',
|
|
340
|
+
'id',
|
|
341
|
+
'input',
|
|
342
|
+
'module',
|
|
343
|
+
'rootDir',
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
const directInputEntries = (
|
|
347
|
+
input: RunTrailInput
|
|
348
|
+
): readonly (readonly [string, unknown])[] =>
|
|
349
|
+
Object.entries(input).filter(([key]) => !RUN_TRAIL_CONTROL_KEYS.has(key));
|
|
350
|
+
|
|
351
|
+
const resolveInnerTrailInput = (
|
|
352
|
+
input: RunTrailInput
|
|
353
|
+
): Result<unknown, ValidationError> => {
|
|
354
|
+
const entries = directInputEntries(input);
|
|
355
|
+
if (entries.length === 0) {
|
|
356
|
+
return Result.ok(input.input);
|
|
357
|
+
}
|
|
358
|
+
if (Object.hasOwn(input, 'input')) {
|
|
359
|
+
return Result.err(
|
|
360
|
+
new ValidationError(
|
|
361
|
+
'trails run received both direct input fields and an explicit input wrapper. Use one shape: either {"name":"Alpha"} or {"input":{"name":"Alpha"}}.'
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return Result.ok(Object.fromEntries(entries));
|
|
366
|
+
};
|
|
367
|
+
|
|
336
368
|
// ---------------------------------------------------------------------------
|
|
337
369
|
// Trail definition
|
|
338
370
|
// ---------------------------------------------------------------------------
|
|
339
371
|
|
|
340
372
|
export const runTrail = trail('run', {
|
|
341
373
|
args: ['id'],
|
|
342
|
-
blaze: async (input: RunTrailInput, ctx) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Single-app back-compat: if the caller provided `module`, trust it.
|
|
350
|
-
const moduleResolution = await resolveRunModulePath(
|
|
351
|
-
rootDir,
|
|
352
|
-
input.module,
|
|
353
|
-
input.id,
|
|
354
|
-
input.app
|
|
355
|
-
);
|
|
356
|
-
if (moduleResolution.isErr()) {
|
|
357
|
-
return moduleResolution;
|
|
358
|
-
}
|
|
359
|
-
const modulePath = moduleResolution.value;
|
|
360
|
-
|
|
361
|
-
const leaseResult = await tryLoadFreshAppLease(modulePath, rootDir);
|
|
362
|
-
if (leaseResult.isErr()) {
|
|
363
|
-
return leaseResult;
|
|
364
|
-
}
|
|
365
|
-
const lease = leaseResult.value;
|
|
374
|
+
blaze: async (input: RunTrailInput, ctx) =>
|
|
375
|
+
withOperatorRootDir(input, ctx, async (rootDir) => {
|
|
376
|
+
const innerInput = resolveInnerTrailInput(input);
|
|
377
|
+
if (innerInput.isErr()) {
|
|
378
|
+
return innerInput;
|
|
379
|
+
}
|
|
366
380
|
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
381
|
+
// Single-app back-compat: if the caller provided `module`, trust it.
|
|
382
|
+
const moduleResolution = await resolveRunModulePath(
|
|
383
|
+
rootDir,
|
|
384
|
+
input.module,
|
|
385
|
+
input.id,
|
|
386
|
+
input.app
|
|
387
|
+
);
|
|
388
|
+
if (moduleResolution.isErr()) {
|
|
389
|
+
return moduleResolution;
|
|
373
390
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
391
|
+
const modulePath = moduleResolution.value;
|
|
392
|
+
|
|
393
|
+
return withFreshAppLease(modulePath, rootDir, async (lease) => {
|
|
394
|
+
const result = await run(lease.app, input.id, innerInput.value, {
|
|
395
|
+
ctx: ctx.permit === undefined ? {} : { permit: ctx.permit },
|
|
396
|
+
});
|
|
397
|
+
if (result.isErr()) {
|
|
398
|
+
return result;
|
|
399
|
+
}
|
|
400
|
+
return Result.ok({
|
|
401
|
+
kind: INNER_TRAIL_RESULT_KIND,
|
|
402
|
+
trailId: input.id,
|
|
403
|
+
value: result.value,
|
|
404
|
+
});
|
|
378
405
|
});
|
|
379
|
-
}
|
|
380
|
-
lease.release();
|
|
381
|
-
}
|
|
382
|
-
},
|
|
406
|
+
}),
|
|
383
407
|
description:
|
|
384
408
|
'Resolve a trail by ID in the current app and execute it through the shared pipeline',
|
|
385
409
|
examples: [
|