@ontrails/trails 0.2.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.
- package/CHANGELOG.md +1906 -0
- package/README.md +48 -0
- package/bin/trails.ts +3 -0
- package/package.json +57 -0
- package/src/app.ts +167 -0
- package/src/clack.ts +111 -0
- package/src/cli.ts +308 -0
- package/src/completions.ts +431 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +129 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +92 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/audit.ts +571 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +636 -0
- package/src/regrade/lifecycle.ts +76 -0
- package/src/regrade/live-api-preserve.ts +123 -0
- package/src/regrade/plan-artifact.ts +515 -0
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/prepared-run.ts +259 -0
- package/src/regrade/receipt-history.ts +446 -0
- package/src/regrade/source-transaction.ts +185 -0
- package/src/release/bindings.ts +58 -0
- package/src/release/changeset-packages.ts +99 -0
- package/src/release/check.ts +1191 -0
- package/src/release/cli-bundle.ts +575 -0
- package/src/release/config.ts +73 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/homebrew.ts +221 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +255 -0
- package/src/release/lock-roundtrip-workspace.ts +107 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +848 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1780 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/stable-version-release.ts +80 -0
- package/src/release/wayfinder-dogfood-smoke.ts +762 -0
- package/src/release/zero-line-transition.ts +68 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-argv.ts +133 -0
- package/src/run-collision.ts +126 -0
- package/src/run-completions-install.ts +179 -0
- package/src/run-example.ts +149 -0
- package/src/run-examples.ts +148 -0
- package/src/run-quiet.ts +75 -0
- package/src/run-regrade-progress.ts +47 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.ts +74 -0
- package/src/run-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch-project.ts +52 -0
- package/src/run-watch.ts +381 -0
- package/src/run-wayfind-outline.ts +170 -0
- package/src/scaffold-version-sync.ts +183 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +816 -0
- package/src/trails/add-trail.ts +141 -0
- package/src/trails/add-verify.ts +252 -0
- package/src/trails/compile.ts +118 -0
- package/src/trails/completions-complete.ts +236 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/config-explain.ts +43 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +1215 -0
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +652 -0
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +80 -0
- package/src/trails/dev-reset.ts +48 -0
- package/src/trails/dev-stats.ts +71 -0
- package/src/trails/dev-support.ts +360 -0
- package/src/trails/doctor.ts +77 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +106 -0
- package/src/trails/load-app.ts +1145 -0
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project-context-output.ts +304 -0
- package/src/trails/project-context.ts +613 -0
- package/src/trails/project.ts +65 -0
- package/src/trails/regrade.ts +4951 -0
- package/src/trails/release-check.ts +113 -0
- package/src/trails/release-smoke.ts +49 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +592 -0
- package/src/trails/run-examples.ts +149 -0
- package/src/trails/run.ts +496 -0
- package/src/trails/scaffold-json.ts +60 -0
- package/src/trails/scaffold-topo-identity.ts +479 -0
- package/src/trails/survey.ts +990 -0
- package/src/trails/topo-activation.ts +14 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +259 -0
- package/src/trails/topo-pin.ts +38 -0
- package/src/trails/topo-read-support.ts +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +323 -0
- package/src/trails/topo-support.ts +247 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +348 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +598 -0
- package/src/trails/wayfind-diff.ts +716 -0
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1319 -0
- package/src/versions.ts +31 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run.examples` trail -- list examples for a target trail.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
NotFoundError,
|
|
7
|
+
Result,
|
|
8
|
+
deriveStructuredTrailExamples,
|
|
9
|
+
trail,
|
|
10
|
+
} from '@ontrails/core';
|
|
11
|
+
import type { StructuredTrailExample, Topo } from '@ontrails/core';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
import { withFreshAppLease } from './operator-context.js';
|
|
15
|
+
import { assertConfiguredAppBinding } from './project-context.js';
|
|
16
|
+
import {
|
|
17
|
+
operatorProjectContextOutput,
|
|
18
|
+
operatorProjectContextOutputSchema,
|
|
19
|
+
} from './project-context-output.js';
|
|
20
|
+
import { resolveRunTargetProject } from './run.js';
|
|
21
|
+
import { createCurrentAppExampleInput } from './topo-support.js';
|
|
22
|
+
|
|
23
|
+
export const RUN_EXAMPLES_LISTING_KIND = 'examples-listing' as const;
|
|
24
|
+
|
|
25
|
+
export const structuredTrailExampleSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
description: z.string().optional(),
|
|
28
|
+
error: z.string().optional(),
|
|
29
|
+
expected: z.unknown().optional(),
|
|
30
|
+
expectedMatch: z.unknown().optional(),
|
|
31
|
+
input: z.unknown(),
|
|
32
|
+
kind: z.union([z.literal('success'), z.literal('error')]),
|
|
33
|
+
name: z.string(),
|
|
34
|
+
provenance: z.object({ source: z.literal('trail.examples') }),
|
|
35
|
+
signals: z
|
|
36
|
+
.array(
|
|
37
|
+
z.object({
|
|
38
|
+
payload: z.unknown().optional(),
|
|
39
|
+
payloadMatch: z.unknown().optional(),
|
|
40
|
+
signalId: z.string(),
|
|
41
|
+
times: z.number().optional(),
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.readonly()
|
|
45
|
+
.optional(),
|
|
46
|
+
})
|
|
47
|
+
.passthrough();
|
|
48
|
+
|
|
49
|
+
export const runExamplesListingSchema = z.object({
|
|
50
|
+
examples: z.array(structuredTrailExampleSchema).readonly(),
|
|
51
|
+
executedAppId: z.string(),
|
|
52
|
+
kind: z.literal(RUN_EXAMPLES_LISTING_KIND),
|
|
53
|
+
project: operatorProjectContextOutputSchema,
|
|
54
|
+
trailId: z.string(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export type RunExamplesListing = z.infer<typeof runExamplesListingSchema>;
|
|
58
|
+
|
|
59
|
+
const buildHappyExampleInput = (): {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly module: string;
|
|
62
|
+
readonly rootDir: string;
|
|
63
|
+
} => ({
|
|
64
|
+
...createCurrentAppExampleInput({ selection: 'configured-project' }),
|
|
65
|
+
id: 'survey.brief',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const buildExamplesListing = (
|
|
69
|
+
app: Topo,
|
|
70
|
+
trailId: string,
|
|
71
|
+
project: RunExamplesListing['project']
|
|
72
|
+
): Result<RunExamplesListing, Error> => {
|
|
73
|
+
const target = app.get(trailId);
|
|
74
|
+
if (target === undefined) {
|
|
75
|
+
return Result.err(
|
|
76
|
+
new NotFoundError(
|
|
77
|
+
`Trail '${trailId}' was not found in the resolved app.`,
|
|
78
|
+
{ context: { trailId } }
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const structured =
|
|
84
|
+
(deriveStructuredTrailExamples(target.examples) as
|
|
85
|
+
| readonly StructuredTrailExample[]
|
|
86
|
+
| undefined) ?? [];
|
|
87
|
+
return Result.ok({
|
|
88
|
+
examples: structured as unknown as RunExamplesListing['examples'],
|
|
89
|
+
executedAppId: app.name,
|
|
90
|
+
kind: RUN_EXAMPLES_LISTING_KIND,
|
|
91
|
+
project,
|
|
92
|
+
trailId,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const runExamplesTrailInputSchema = z.object({
|
|
97
|
+
app: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe(
|
|
101
|
+
'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
|
|
102
|
+
),
|
|
103
|
+
id: z.string().describe('Trail ID whose examples should be listed'),
|
|
104
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
105
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
type RunExamplesTrailInput = z.output<typeof runExamplesTrailInputSchema>;
|
|
109
|
+
|
|
110
|
+
export const runExamplesTrail = trail('run.examples', {
|
|
111
|
+
args: ['id'],
|
|
112
|
+
description: "List a trail's examples without executing it",
|
|
113
|
+
examples: [
|
|
114
|
+
{
|
|
115
|
+
description: 'List examples authored on a target trail',
|
|
116
|
+
input: buildHappyExampleInput(),
|
|
117
|
+
name: 'List trail examples',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
implementation: async (input: RunExamplesTrailInput, ctx) => {
|
|
121
|
+
const target = await resolveRunTargetProject(input, input.id, {
|
|
122
|
+
cwd: ctx.cwd,
|
|
123
|
+
});
|
|
124
|
+
if (target.isErr()) {
|
|
125
|
+
return target;
|
|
126
|
+
}
|
|
127
|
+
return withFreshAppLease(
|
|
128
|
+
target.value.modulePath,
|
|
129
|
+
target.value.rootDir,
|
|
130
|
+
(lease) => {
|
|
131
|
+
const binding = assertConfiguredAppBinding(
|
|
132
|
+
target.value.context,
|
|
133
|
+
lease.app.name
|
|
134
|
+
);
|
|
135
|
+
if (binding.isErr()) {
|
|
136
|
+
return binding;
|
|
137
|
+
}
|
|
138
|
+
return buildExamplesListing(
|
|
139
|
+
lease.app,
|
|
140
|
+
input.id,
|
|
141
|
+
operatorProjectContextOutput(target.value.context)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
input: runExamplesTrailInputSchema,
|
|
147
|
+
intent: 'read',
|
|
148
|
+
output: runExamplesListingSchema,
|
|
149
|
+
});
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run` trail -- Direct trail invocation by ID.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a trail through the shared operator project context and executes it
|
|
5
|
+
* through the `run()` pipeline from `@ontrails/core`. The CLI surface drives
|
|
6
|
+
* this trail with `trails run <id> [--app <id>] [inline-json]`.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
*
|
|
10
|
+
* 1. Resolve Config-owned project/app identity through the shared selector.
|
|
11
|
+
* 2. A selected app loads only that app for execution; an absent ID receives
|
|
12
|
+
* wrong-app or collision coaching from a complete live Config-owned view.
|
|
13
|
+
* Unavailable unrelated apps leave that coaching explicitly incomplete
|
|
14
|
+
* without replacing the selected app's result.
|
|
15
|
+
* 3. An unqualified workspace selection derives the complete ownership view
|
|
16
|
+
* live before choosing one owner. Missing apps fail closed and collisions
|
|
17
|
+
* return `AmbiguousError` for the CLI's optional TTY recovery.
|
|
18
|
+
* 4. `module` refines the selected app entry; it never replaces the selected
|
|
19
|
+
* app identity or collection boundary.
|
|
20
|
+
*
|
|
21
|
+
* The trail's output keeps a typed discriminator around the heterogeneous
|
|
22
|
+
* inner trail value. The value itself remains `unknown` because direct
|
|
23
|
+
* invocation can target any trail in the loaded app.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
AmbiguousError,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
Result,
|
|
32
|
+
ValidationError,
|
|
33
|
+
run,
|
|
34
|
+
trail,
|
|
35
|
+
} from '@ontrails/core';
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
|
|
38
|
+
import { withFreshAppLease } from './operator-context.js';
|
|
39
|
+
import {
|
|
40
|
+
assertConfiguredAppBinding,
|
|
41
|
+
resolveOperatorAppModuleContext,
|
|
42
|
+
resolveOperatorProjectContext,
|
|
43
|
+
} from './project-context.js';
|
|
44
|
+
import type {
|
|
45
|
+
OperatorAppProjectContext,
|
|
46
|
+
OperatorProjectApp,
|
|
47
|
+
OperatorProjectContext,
|
|
48
|
+
OperatorWorkspaceProjectContext,
|
|
49
|
+
} from './project-context.js';
|
|
50
|
+
import {
|
|
51
|
+
operatorProjectContextOutput,
|
|
52
|
+
operatorProjectContextOutputSchema,
|
|
53
|
+
} from './project-context-output.js';
|
|
54
|
+
import { createCurrentAppExampleInput } from './topo-support.js';
|
|
55
|
+
|
|
56
|
+
export const INNER_TRAIL_RESULT_KIND = 'inner-trail-result' as const;
|
|
57
|
+
|
|
58
|
+
export const innerTrailResultSchema = z.object({
|
|
59
|
+
executedAppId: z.string(),
|
|
60
|
+
kind: z.literal(INNER_TRAIL_RESULT_KIND),
|
|
61
|
+
project: operatorProjectContextOutputSchema,
|
|
62
|
+
trailId: z.string(),
|
|
63
|
+
value: z.unknown(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type InnerTrailResult = z.infer<typeof innerTrailResultSchema>;
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Resolution outcomes
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const ambiguousMessage = (
|
|
73
|
+
trailId: string,
|
|
74
|
+
candidates: readonly string[]
|
|
75
|
+
): string =>
|
|
76
|
+
`Trail ID '${trailId}' exists in apps: ${candidates.join(', ')}. Re-run with --app <id>.`;
|
|
77
|
+
|
|
78
|
+
const configuredApps = (
|
|
79
|
+
context: OperatorProjectContext
|
|
80
|
+
): readonly OperatorProjectApp[] =>
|
|
81
|
+
context.selectedExtent === 'workspace'
|
|
82
|
+
? context.apps
|
|
83
|
+
: context.identity.apps.map((app) => ({
|
|
84
|
+
configured: true,
|
|
85
|
+
id: app.id,
|
|
86
|
+
lockPath: join(app.rootDir, 'trails.lock'),
|
|
87
|
+
modulePath: app.entry,
|
|
88
|
+
moduleSource: app.entrySource === 'explicit' ? 'config' : 'convention',
|
|
89
|
+
root: app.root,
|
|
90
|
+
rootDir: app.rootDir,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
interface LiveOwnershipView {
|
|
94
|
+
readonly owners: readonly string[];
|
|
95
|
+
readonly unavailableAppIds: readonly string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const collectLiveOwners = async (
|
|
99
|
+
context: OperatorProjectContext,
|
|
100
|
+
trailId: string
|
|
101
|
+
): Promise<Result<LiveOwnershipView, Error>> => {
|
|
102
|
+
const owners: string[] = [];
|
|
103
|
+
const unavailableAppIds: string[] = [];
|
|
104
|
+
for (const app of configuredApps(context)) {
|
|
105
|
+
if (app.id === undefined) {
|
|
106
|
+
return Result.err(
|
|
107
|
+
new ValidationError(
|
|
108
|
+
'Configured ownership discovery requires a stable app ID.',
|
|
109
|
+
{
|
|
110
|
+
context: {
|
|
111
|
+
modulePath: app.modulePath,
|
|
112
|
+
reason: 'invalid-binding',
|
|
113
|
+
rootDir: app.rootDir,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const appId = app.id;
|
|
120
|
+
const loaded = await withFreshAppLease(
|
|
121
|
+
app.modulePath,
|
|
122
|
+
app.rootDir,
|
|
123
|
+
(lease) => {
|
|
124
|
+
if (lease.app.name !== appId) {
|
|
125
|
+
return Result.err(
|
|
126
|
+
new ValidationError(
|
|
127
|
+
`Configured app "${appId}" loaded topo "${lease.app.name}" while resolving trail ownership.`,
|
|
128
|
+
{
|
|
129
|
+
context: {
|
|
130
|
+
actualAppId: lease.app.name,
|
|
131
|
+
expectedAppId: appId,
|
|
132
|
+
modulePath: app.modulePath,
|
|
133
|
+
reason: 'invalid-binding',
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (lease.app.get(trailId) !== undefined) {
|
|
140
|
+
owners.push(appId);
|
|
141
|
+
}
|
|
142
|
+
return Result.ok();
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
if (loaded.isErr()) {
|
|
146
|
+
if (context.selectedExtent === 'workspace') {
|
|
147
|
+
return loaded;
|
|
148
|
+
}
|
|
149
|
+
unavailableAppIds.push(appId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return Result.ok({
|
|
153
|
+
owners: owners.toSorted(),
|
|
154
|
+
unavailableAppIds: unavailableAppIds.toSorted(),
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const ownerError = (
|
|
159
|
+
context: OperatorProjectContext,
|
|
160
|
+
trailId: string,
|
|
161
|
+
owners: readonly string[]
|
|
162
|
+
): Error => {
|
|
163
|
+
const requestedApp =
|
|
164
|
+
context.selectedExtent === 'configured-app' ? context.app.id : undefined;
|
|
165
|
+
const [actualOwner] = owners;
|
|
166
|
+
if (
|
|
167
|
+
requestedApp !== undefined &&
|
|
168
|
+
owners.includes(requestedApp) &&
|
|
169
|
+
context.selectedExtent === 'configured-app'
|
|
170
|
+
) {
|
|
171
|
+
return new NotFoundError(
|
|
172
|
+
`Trail '${trailId}' is not exposed by module '${context.app.modulePath}' selected for app '${requestedApp}', but the app's Config-owned default entry does expose it. Remove --module or select a module that exposes the trail.`,
|
|
173
|
+
{
|
|
174
|
+
context: {
|
|
175
|
+
modulePath: context.app.modulePath,
|
|
176
|
+
requestedApp,
|
|
177
|
+
trailId,
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (owners.length > 1) {
|
|
183
|
+
return new AmbiguousError(ambiguousMessage(trailId, owners), {
|
|
184
|
+
context: { candidates: owners, trailId },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
requestedApp !== undefined &&
|
|
189
|
+
actualOwner !== undefined &&
|
|
190
|
+
actualOwner !== requestedApp
|
|
191
|
+
) {
|
|
192
|
+
return new NotFoundError(
|
|
193
|
+
`Trail '${trailId}' is owned by '${actualOwner}', not '${requestedApp}'.`,
|
|
194
|
+
{ context: { actualOwner, requestedApp, trailId } }
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const appContext =
|
|
198
|
+
requestedApp === undefined ? '' : ` for app '${requestedApp}'`;
|
|
199
|
+
return new NotFoundError(
|
|
200
|
+
`Trail '${trailId}' was not found${appContext} in any configured app under ${context.projectRoot}.`,
|
|
201
|
+
{
|
|
202
|
+
context: {
|
|
203
|
+
...(requestedApp === undefined ? {} : { requestedApp }),
|
|
204
|
+
rootDir: context.projectRoot,
|
|
205
|
+
trailId,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const incompleteConfiguredOwnerError = (
|
|
212
|
+
context: OperatorAppProjectContext,
|
|
213
|
+
trailId: string,
|
|
214
|
+
ownership: LiveOwnershipView
|
|
215
|
+
): Error => {
|
|
216
|
+
const requestedApp = context.app.id;
|
|
217
|
+
if (requestedApp !== undefined && ownership.owners.includes(requestedApp)) {
|
|
218
|
+
return ownerError(context, trailId, [requestedApp]);
|
|
219
|
+
}
|
|
220
|
+
return new NotFoundError(
|
|
221
|
+
`Trail '${trailId}' is not exposed by the selected app${requestedApp === undefined ? '' : ` '${requestedApp}'`}. Ownership coaching is incomplete because configured apps are unavailable: ${ownership.unavailableAppIds.join(', ')}.`,
|
|
222
|
+
{
|
|
223
|
+
context: {
|
|
224
|
+
...(requestedApp === undefined ? {} : { requestedApp }),
|
|
225
|
+
rootDir: context.projectRoot,
|
|
226
|
+
trailId,
|
|
227
|
+
unavailableAppIds: ownership.unavailableAppIds,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const selectedOwnerContext = (
|
|
234
|
+
context: OperatorWorkspaceProjectContext,
|
|
235
|
+
appId: string
|
|
236
|
+
): Result<OperatorAppProjectContext, ValidationError> => {
|
|
237
|
+
const app = context.apps.find((candidate) => candidate.id === appId);
|
|
238
|
+
return app === undefined
|
|
239
|
+
? Result.err(
|
|
240
|
+
new ValidationError(
|
|
241
|
+
`Resolved trail owner "${appId}" is not present in configured project context.`,
|
|
242
|
+
{ context: { appId, reason: 'invalid-binding' } }
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
: Result.ok({
|
|
246
|
+
app,
|
|
247
|
+
boundaryDir: context.boundaryDir,
|
|
248
|
+
identity: context.identity,
|
|
249
|
+
projectRoot: context.projectRoot,
|
|
250
|
+
selectedExtent: 'configured-app',
|
|
251
|
+
selectionProvenance: context.selectionProvenance,
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const resolveRunContext = async (
|
|
256
|
+
context: OperatorProjectContext,
|
|
257
|
+
trailId: string
|
|
258
|
+
): Promise<Result<OperatorAppProjectContext, Error>> => {
|
|
259
|
+
if (context.selectedExtent === 'standalone-app') {
|
|
260
|
+
return resolveOperatorAppModuleContext(context);
|
|
261
|
+
}
|
|
262
|
+
if (context.selectedExtent === 'configured-app') {
|
|
263
|
+
const owns = await withFreshAppLease(
|
|
264
|
+
context.app.modulePath,
|
|
265
|
+
context.app.rootDir,
|
|
266
|
+
(lease) => {
|
|
267
|
+
const binding = assertConfiguredAppBinding(context, lease.app.name);
|
|
268
|
+
return binding.isErr()
|
|
269
|
+
? binding
|
|
270
|
+
: Result.ok(lease.app.get(trailId) !== undefined);
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
if (owns.isErr()) {
|
|
274
|
+
return owns;
|
|
275
|
+
}
|
|
276
|
+
if (owns.value) {
|
|
277
|
+
return Result.ok(context);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const owners = await collectLiveOwners(context, trailId);
|
|
281
|
+
if (owners.isErr()) {
|
|
282
|
+
return owners;
|
|
283
|
+
}
|
|
284
|
+
if (context.selectedExtent === 'configured-app') {
|
|
285
|
+
return Result.err(
|
|
286
|
+
owners.value.unavailableAppIds.length === 0
|
|
287
|
+
? ownerError(context, trailId, owners.value.owners)
|
|
288
|
+
: incompleteConfiguredOwnerError(context, trailId, owners.value)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const [owner] = owners.value.owners;
|
|
292
|
+
if (owners.value.owners.length !== 1 || owner === undefined) {
|
|
293
|
+
return Result.err(ownerError(context, trailId, owners.value.owners));
|
|
294
|
+
}
|
|
295
|
+
return context.selectedExtent === 'workspace'
|
|
296
|
+
? selectedOwnerContext(context, owner)
|
|
297
|
+
: Result.err(
|
|
298
|
+
new ValidationError('Unable to resolve workspace run context.', {
|
|
299
|
+
context: { reason: 'invalid-binding' },
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
interface RunTargetInput {
|
|
305
|
+
readonly app?: string | undefined;
|
|
306
|
+
readonly module?: string | undefined;
|
|
307
|
+
readonly rootDir?: string | undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
interface RunTargetRuntime {
|
|
311
|
+
readonly cwd?: string | undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface RunTargetProject {
|
|
315
|
+
readonly context: OperatorAppProjectContext;
|
|
316
|
+
readonly modulePath: string;
|
|
317
|
+
readonly rootDir: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Resolve one run target without collapsing inferred CWD into explicit root selection. */
|
|
321
|
+
export const resolveRunTargetProject = async (
|
|
322
|
+
input: RunTargetInput,
|
|
323
|
+
trailId: string,
|
|
324
|
+
runtime: RunTargetRuntime
|
|
325
|
+
): Promise<Result<RunTargetProject, Error>> => {
|
|
326
|
+
const context = await resolveOperatorProjectContext(input, runtime);
|
|
327
|
+
if (context.isErr()) {
|
|
328
|
+
return context;
|
|
329
|
+
}
|
|
330
|
+
const selected = await resolveRunContext(context.value, trailId);
|
|
331
|
+
if (selected.isErr()) {
|
|
332
|
+
return selected;
|
|
333
|
+
}
|
|
334
|
+
return Result.ok({
|
|
335
|
+
context: selected.value,
|
|
336
|
+
modulePath: selected.value.app.modulePath,
|
|
337
|
+
rootDir: selected.value.app.rootDir,
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Example input helpers
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
const buildHappyExampleInput = (): {
|
|
346
|
+
readonly input: { readonly module: string; readonly rootDir: string };
|
|
347
|
+
readonly id: string;
|
|
348
|
+
readonly module: string;
|
|
349
|
+
readonly rootDir: string;
|
|
350
|
+
} => {
|
|
351
|
+
const isolated = createCurrentAppExampleInput();
|
|
352
|
+
const configured = createCurrentAppExampleInput({
|
|
353
|
+
selection: 'configured-project',
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
id: 'survey.brief',
|
|
357
|
+
input: isolated,
|
|
358
|
+
...configured,
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const buildNotFoundExampleInput = (): {
|
|
363
|
+
readonly id: string;
|
|
364
|
+
readonly module: string;
|
|
365
|
+
readonly rootDir: string;
|
|
366
|
+
} => ({
|
|
367
|
+
...createCurrentAppExampleInput({ selection: 'configured-project' }),
|
|
368
|
+
id: 'does.not.exist',
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const runTrailInputSchema = z
|
|
372
|
+
.object({
|
|
373
|
+
app: z
|
|
374
|
+
.string()
|
|
375
|
+
.optional()
|
|
376
|
+
.describe(
|
|
377
|
+
'Workspace app to resolve the trail ID against; required when the ID is exposed by more than one app'
|
|
378
|
+
),
|
|
379
|
+
id: z.string().describe('Trail ID to invoke'),
|
|
380
|
+
input: z
|
|
381
|
+
.unknown()
|
|
382
|
+
.optional()
|
|
383
|
+
.describe(
|
|
384
|
+
'Parsed input for the resolved trail; the CLI surface JSON.parses the inline argument before passing it through'
|
|
385
|
+
),
|
|
386
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
387
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
388
|
+
})
|
|
389
|
+
.catchall(z.unknown());
|
|
390
|
+
|
|
391
|
+
type RunTrailInput = z.output<typeof runTrailInputSchema>;
|
|
392
|
+
|
|
393
|
+
const RUN_TRAIL_CONTROL_KEYS = new Set([
|
|
394
|
+
'app',
|
|
395
|
+
'id',
|
|
396
|
+
'input',
|
|
397
|
+
'module',
|
|
398
|
+
'rootDir',
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
const directInputEntries = (
|
|
402
|
+
input: RunTrailInput
|
|
403
|
+
): readonly (readonly [string, unknown])[] =>
|
|
404
|
+
Object.entries(input).filter(([key]) => !RUN_TRAIL_CONTROL_KEYS.has(key));
|
|
405
|
+
|
|
406
|
+
const resolveInnerTrailInput = (
|
|
407
|
+
input: RunTrailInput
|
|
408
|
+
): Result<unknown, ValidationError> => {
|
|
409
|
+
const entries = directInputEntries(input);
|
|
410
|
+
if (entries.length === 0) {
|
|
411
|
+
return Result.ok(input.input);
|
|
412
|
+
}
|
|
413
|
+
if (Object.hasOwn(input, 'input')) {
|
|
414
|
+
return Result.err(
|
|
415
|
+
new ValidationError(
|
|
416
|
+
'trails run received both direct input fields and an explicit input wrapper. Use one shape: either {"name":"Alpha"} or {"input":{"name":"Alpha"}}.'
|
|
417
|
+
)
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return Result.ok(Object.fromEntries(entries));
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Trail definition
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
export const runTrail = trail('run', {
|
|
428
|
+
args: ['id'],
|
|
429
|
+
description:
|
|
430
|
+
'Resolve a trail by ID in the current app and execute it through the shared pipeline',
|
|
431
|
+
examples: [
|
|
432
|
+
{
|
|
433
|
+
description:
|
|
434
|
+
'Resolve and execute a trail by ID, returning the inner trail Result value',
|
|
435
|
+
input: buildHappyExampleInput(),
|
|
436
|
+
name: 'Run trail by ID',
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
description: 'Reject an unknown trail ID with NotFoundError',
|
|
440
|
+
error: 'NotFoundError',
|
|
441
|
+
input: buildNotFoundExampleInput(),
|
|
442
|
+
name: 'Reject unknown trail ID',
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
implementation: async (
|
|
446
|
+
input: RunTrailInput,
|
|
447
|
+
ctx
|
|
448
|
+
): Promise<Result<InnerTrailResult, Error>> => {
|
|
449
|
+
const contextResult = await resolveOperatorProjectContext(input, {
|
|
450
|
+
cwd: ctx.cwd,
|
|
451
|
+
});
|
|
452
|
+
if (contextResult.isErr()) {
|
|
453
|
+
return contextResult;
|
|
454
|
+
}
|
|
455
|
+
const context = contextResult.value;
|
|
456
|
+
const resolved = await resolveRunContext(context, input.id);
|
|
457
|
+
if (resolved.isErr()) {
|
|
458
|
+
return resolved;
|
|
459
|
+
}
|
|
460
|
+
const selected = resolved.value;
|
|
461
|
+
const innerInput = resolveInnerTrailInput(input);
|
|
462
|
+
if (innerInput.isErr()) {
|
|
463
|
+
return innerInput;
|
|
464
|
+
}
|
|
465
|
+
return withFreshAppLease(
|
|
466
|
+
selected.app.modulePath,
|
|
467
|
+
selected.app.rootDir,
|
|
468
|
+
async (lease) => {
|
|
469
|
+
const binding = assertConfiguredAppBinding(selected, lease.app.name);
|
|
470
|
+
if (binding.isErr()) {
|
|
471
|
+
return binding;
|
|
472
|
+
}
|
|
473
|
+
const result = await run(lease.app, input.id, innerInput.value, {
|
|
474
|
+
ctx: {
|
|
475
|
+
cwd: selected.app.rootDir,
|
|
476
|
+
...(ctx.permit === undefined ? {} : { permit: ctx.permit }),
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
if (result.isErr()) {
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
return Result.ok({
|
|
483
|
+
executedAppId: lease.app.name,
|
|
484
|
+
kind: INNER_TRAIL_RESULT_KIND,
|
|
485
|
+
project: operatorProjectContextOutput(selected),
|
|
486
|
+
trailId: input.id,
|
|
487
|
+
value: result.value,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
},
|
|
492
|
+
input: runTrailInputSchema,
|
|
493
|
+
intent: 'write',
|
|
494
|
+
output: innerTrailResultSchema,
|
|
495
|
+
permit: { scopes: ['trails:run'] },
|
|
496
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const packageKeyOrder = [
|
|
2
|
+
'name',
|
|
3
|
+
'version',
|
|
4
|
+
'private',
|
|
5
|
+
'bin',
|
|
6
|
+
'type',
|
|
7
|
+
'workspaces',
|
|
8
|
+
'scripts',
|
|
9
|
+
'dependencies',
|
|
10
|
+
'devDependencies',
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
const packageMapKeys = new Set<string>([
|
|
14
|
+
'bin',
|
|
15
|
+
'dependencies',
|
|
16
|
+
'devDependencies',
|
|
17
|
+
'scripts',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export type ScaffoldPackageJson = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
|
|
23
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
|
|
25
|
+
const sortRecord = (record: Record<string, unknown>): Record<string, unknown> =>
|
|
26
|
+
Object.fromEntries(
|
|
27
|
+
Object.entries(record).toSorted(([left], [right]) =>
|
|
28
|
+
left.localeCompare(right)
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const normalizePackageValue = (key: string, value: unknown): unknown =>
|
|
33
|
+
packageMapKeys.has(key) && isPlainRecord(value) ? sortRecord(value) : value;
|
|
34
|
+
|
|
35
|
+
export const normalizeScaffoldPackageJson = (
|
|
36
|
+
pkg: ScaffoldPackageJson
|
|
37
|
+
): ScaffoldPackageJson => {
|
|
38
|
+
const normalized: ScaffoldPackageJson = {};
|
|
39
|
+
|
|
40
|
+
for (const key of packageKeyOrder) {
|
|
41
|
+
if (pkg[key] !== undefined) {
|
|
42
|
+
normalized[key] = normalizePackageValue(key, pkg[key]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(pkg).toSorted()) {
|
|
47
|
+
if (!(key in normalized)) {
|
|
48
|
+
normalized[key] = normalizePackageValue(key, pkg[key]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return normalized;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const stringifyScaffoldJson = (value: unknown): string =>
|
|
56
|
+
`${JSON.stringify(value, null, 2)}\n`;
|
|
57
|
+
|
|
58
|
+
export const stringifyScaffoldPackageJson = (
|
|
59
|
+
pkg: ScaffoldPackageJson
|
|
60
|
+
): string => stringifyScaffoldJson(normalizeScaffoldPackageJson(pkg));
|