@ontrails/trails 1.0.0-beta.14 → 1.0.0-beta.16
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 +208 -0
- package/README.md +27 -0
- package/package.json +19 -8
- package/src/app.ts +17 -7
- package/src/clack.ts +1 -1
- package/src/cli.ts +304 -10
- package/src/completions.ts +240 -0
- package/src/load-app-mirror.ts +160 -0
- package/src/local-state-io.ts +153 -0
- package/src/project-writes.ts +320 -0
- package/src/run-collision.ts +125 -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-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/add-surface.ts +172 -0
- package/src/trails/add-trail.ts +73 -27
- package/src/trails/add-verify.ts +68 -23
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-scaffold.ts +101 -35
- package/src/trails/create.ts +87 -74
- package/src/trails/dev-clean.ts +31 -22
- package/src/trails/dev-reset.ts +9 -3
- package/src/trails/dev-stats.ts +28 -20
- package/src/trails/dev-support.ts +109 -95
- package/src/trails/draft-promote.ts +351 -107
- package/src/trails/guide.ts +55 -38
- package/src/trails/load-app.ts +712 -38
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +482 -0
- package/src/trails/run-examples.ts +141 -0
- package/src/trails/run.ts +403 -0
- package/src/trails/survey.ts +517 -186
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-compile.ts +55 -0
- package/src/trails/topo-history.ts +14 -11
- package/src/trails/topo-output-schemas.ts +175 -0
- package/src/trails/topo-pin.ts +25 -16
- package/src/trails/topo-read-support.ts +178 -238
- package/src/trails/topo-reports.ts +445 -63
- package/src/trails/topo-store-support.ts +67 -35
- package/src/trails/topo-support.ts +93 -147
- package/src/trails/topo-unpin.ts +17 -7
- package/src/trails/topo-verify.ts +19 -10
- package/src/trails/topo.ts +64 -31
- package/src/trails/warden-guide.ts +121 -0
- package/src/trails/warden.ts +137 -47
- package/src/versions.ts +28 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -20
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -22
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -13
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -88
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -10
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -77
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-trailhead.d.ts +0 -13
- package/dist/src/trails/add-trailhead.d.ts.map +0 -1
- package/dist/src/trails/add-trailhead.js +0 -88
- package/dist/src/trails/add-trailhead.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -67
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -288
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -22
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -121
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/dev-clean.d.ts +0 -9
- package/dist/src/trails/dev-clean.d.ts.map +0 -1
- package/dist/src/trails/dev-clean.js +0 -65
- package/dist/src/trails/dev-clean.js.map +0 -1
- package/dist/src/trails/dev-reset.d.ts +0 -6
- package/dist/src/trails/dev-reset.d.ts.map +0 -1
- package/dist/src/trails/dev-reset.js +0 -38
- package/dist/src/trails/dev-reset.js.map +0 -1
- package/dist/src/trails/dev-stats.d.ts +0 -7
- package/dist/src/trails/dev-stats.d.ts.map +0 -1
- package/dist/src/trails/dev-stats.js +0 -61
- package/dist/src/trails/dev-stats.js.map +0 -1
- package/dist/src/trails/dev-support.d.ts +0 -64
- package/dist/src/trails/dev-support.d.ts.map +0 -1
- package/dist/src/trails/dev-support.js +0 -178
- package/dist/src/trails/dev-support.js.map +0 -1
- package/dist/src/trails/draft-promote.d.ts +0 -18
- package/dist/src/trails/draft-promote.d.ts.map +0 -1
- package/dist/src/trails/draft-promote.js +0 -386
- package/dist/src/trails/draft-promote.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -21
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -64
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -6
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -67
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -54
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -18
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -212
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/topo-constants.d.ts +0 -3
- package/dist/src/trails/topo-constants.d.ts.map +0 -1
- package/dist/src/trails/topo-constants.js +0 -3
- package/dist/src/trails/topo-constants.js.map +0 -1
- package/dist/src/trails/topo-export.d.ts +0 -18
- package/dist/src/trails/topo-export.d.ts.map +0 -1
- package/dist/src/trails/topo-export.js +0 -34
- package/dist/src/trails/topo-export.js.map +0 -1
- package/dist/src/trails/topo-history.d.ts +0 -24
- package/dist/src/trails/topo-history.d.ts.map +0 -1
- package/dist/src/trails/topo-history.js +0 -33
- package/dist/src/trails/topo-history.js.map +0 -1
- package/dist/src/trails/topo-pin.d.ts +0 -21
- package/dist/src/trails/topo-pin.d.ts.map +0 -1
- package/dist/src/trails/topo-pin.js +0 -35
- package/dist/src/trails/topo-pin.js.map +0 -1
- package/dist/src/trails/topo-read-support.d.ts +0 -54
- package/dist/src/trails/topo-read-support.d.ts.map +0 -1
- package/dist/src/trails/topo-read-support.js +0 -178
- package/dist/src/trails/topo-read-support.js.map +0 -1
- package/dist/src/trails/topo-reports.d.ts +0 -50
- package/dist/src/trails/topo-reports.d.ts.map +0 -1
- package/dist/src/trails/topo-reports.js +0 -122
- package/dist/src/trails/topo-reports.js.map +0 -1
- package/dist/src/trails/topo-show.d.ts +0 -23
- package/dist/src/trails/topo-show.d.ts.map +0 -1
- package/dist/src/trails/topo-show.js +0 -53
- package/dist/src/trails/topo-show.js.map +0 -1
- package/dist/src/trails/topo-store-support.d.ts +0 -13
- package/dist/src/trails/topo-store-support.d.ts.map +0 -1
- package/dist/src/trails/topo-store-support.js +0 -55
- package/dist/src/trails/topo-store-support.js.map +0 -1
- package/dist/src/trails/topo-support.d.ts +0 -87
- package/dist/src/trails/topo-support.d.ts.map +0 -1
- package/dist/src/trails/topo-support.js +0 -165
- package/dist/src/trails/topo-support.js.map +0 -1
- package/dist/src/trails/topo-unpin.d.ts +0 -15
- package/dist/src/trails/topo-unpin.d.ts.map +0 -1
- package/dist/src/trails/topo-unpin.js +0 -39
- package/dist/src/trails/topo-unpin.js.map +0 -1
- package/dist/src/trails/topo-verify.d.ts +0 -5
- package/dist/src/trails/topo-verify.d.ts.map +0 -1
- package/dist/src/trails/topo-verify.js +0 -28
- package/dist/src/trails/topo-verify.js.map +0 -1
- package/dist/src/trails/topo.d.ts +0 -5
- package/dist/src/trails/topo.d.ts.map +0 -1
- package/dist/src/trails/topo.js +0 -67
- package/dist/src/trails/topo.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -19
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -89
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/create.test.ts +0 -351
- package/src/__tests__/draft-promote.test.ts +0 -144
- package/src/__tests__/guide.test.ts +0 -91
- package/src/__tests__/load-app.test.ts +0 -58
- package/src/__tests__/survey.test.ts +0 -301
- package/src/__tests__/topo-dev.test.ts +0 -424
- package/src/__tests__/warden.test.ts +0 -74
- package/src/trails/add-trailhead.ts +0 -121
- package/src/trails/topo-export.ts +0 -39
- package/src/trails/topo-show.ts +0 -58
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI bridge for installing shell completion scripts.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally not a trail: it resolves CLI-local defaults such as
|
|
5
|
+
* `$SHELL` and the user's home directory, then writes to the user's completion
|
|
6
|
+
* directory. The surface-agnostic trail remains `completions`, which renders a
|
|
7
|
+
* script string for any caller.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir } from 'node:fs/promises';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
projectPublicSurfaceError,
|
|
16
|
+
Result,
|
|
17
|
+
ValidationError,
|
|
18
|
+
} from '@ontrails/core';
|
|
19
|
+
import type { Command } from 'commander';
|
|
20
|
+
|
|
21
|
+
import { renderCompletionScript } from './completions.js';
|
|
22
|
+
import type { CompletionShell } from './completions.js';
|
|
23
|
+
|
|
24
|
+
export const COMPLETIONS_BIN_NAME = 'trails';
|
|
25
|
+
|
|
26
|
+
const SHELLS = new Set<CompletionShell>(['bash', 'fish', 'zsh']);
|
|
27
|
+
|
|
28
|
+
const INSTALL_PATH_BY_SHELL: Readonly<Record<CompletionShell, string>> = {
|
|
29
|
+
bash: '.local/share/bash-completion/completions/trails',
|
|
30
|
+
fish: '.config/fish/completions/trails.fish',
|
|
31
|
+
zsh: '.local/share/zsh/site-functions/_trails',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface CompletionsInstallOptions {
|
|
35
|
+
readonly binName?: string | undefined;
|
|
36
|
+
readonly homeDir?: string | undefined;
|
|
37
|
+
readonly shell?: string | undefined;
|
|
38
|
+
readonly shellEnv?: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CompletionsInstallResult {
|
|
42
|
+
readonly created: boolean;
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly shell: CompletionShell;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StdoutLike {
|
|
49
|
+
write(chunk: string): unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AttachCompletionsInstallOptions {
|
|
53
|
+
readonly binName?: string | undefined;
|
|
54
|
+
readonly homeDir?: string | undefined;
|
|
55
|
+
readonly shellEnv?: string | undefined;
|
|
56
|
+
readonly stdout?: StdoutLike | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isCompletionShell = (value: string): value is CompletionShell =>
|
|
60
|
+
SHELLS.has(value as CompletionShell);
|
|
61
|
+
|
|
62
|
+
const detectShellFromEnv = (shellEnv: string): CompletionShell | null => {
|
|
63
|
+
if (shellEnv.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const slashIndex = shellEnv.lastIndexOf('/');
|
|
67
|
+
const base = slashIndex === -1 ? shellEnv : shellEnv.slice(slashIndex + 1);
|
|
68
|
+
return isCompletionShell(base) ? base : null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const unsupportedShellMessage =
|
|
72
|
+
'Could not detect shell from $SHELL. Pass --shell with one of: bash, zsh, fish.';
|
|
73
|
+
|
|
74
|
+
const resolveTargetShell = (input: {
|
|
75
|
+
readonly shell?: string | undefined;
|
|
76
|
+
readonly shellEnv?: string | undefined;
|
|
77
|
+
}): Result<CompletionShell, ValidationError> => {
|
|
78
|
+
if (input.shell !== undefined) {
|
|
79
|
+
if (isCompletionShell(input.shell)) {
|
|
80
|
+
return Result.ok(input.shell);
|
|
81
|
+
}
|
|
82
|
+
return Result.err(
|
|
83
|
+
new ValidationError(
|
|
84
|
+
`Unsupported shell "${input.shell}". Pass one of: bash, zsh, fish.`
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const envValue = input.shellEnv ?? process.env['SHELL'] ?? '';
|
|
89
|
+
const detected = detectShellFromEnv(envValue);
|
|
90
|
+
return detected === null
|
|
91
|
+
? Result.err(new ValidationError(unsupportedShellMessage))
|
|
92
|
+
: Result.ok(detected);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const fileExists = async (path: string): Promise<boolean> =>
|
|
96
|
+
await Bun.file(path).exists();
|
|
97
|
+
|
|
98
|
+
export const runCompletionsInstall = async (
|
|
99
|
+
options: CompletionsInstallOptions = {}
|
|
100
|
+
): Promise<Result<CompletionsInstallResult, Error>> => {
|
|
101
|
+
const shellResult = resolveTargetShell(options);
|
|
102
|
+
if (shellResult.isErr()) {
|
|
103
|
+
return Result.err(shellResult.error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const shell = shellResult.value;
|
|
107
|
+
const home = options.homeDir ?? homedir();
|
|
108
|
+
const path = join(home, INSTALL_PATH_BY_SHELL[shell]);
|
|
109
|
+
const scriptResult = renderCompletionScript(
|
|
110
|
+
shell,
|
|
111
|
+
options.binName ?? COMPLETIONS_BIN_NAME
|
|
112
|
+
);
|
|
113
|
+
if (scriptResult.isErr()) {
|
|
114
|
+
return Result.err(scriptResult.error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let existed: boolean;
|
|
118
|
+
try {
|
|
119
|
+
existed = await fileExists(path);
|
|
120
|
+
await mkdir(dirname(path), { recursive: true });
|
|
121
|
+
await Bun.write(path, scriptResult.value);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return Result.err(
|
|
124
|
+
error instanceof Error ? error : new Error(String(error))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const created = !existed;
|
|
128
|
+
|
|
129
|
+
return Result.ok({
|
|
130
|
+
created,
|
|
131
|
+
message: created
|
|
132
|
+
? `Installed ${shell} completions to ${path}. Run \`exec $SHELL\` or restart your shell to activate.`
|
|
133
|
+
: `Updated ${shell} completions at ${path}.`,
|
|
134
|
+
path,
|
|
135
|
+
shell,
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleCliError = (error: unknown): void => {
|
|
140
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
141
|
+
const projection = projectPublicSurfaceError('cli', err);
|
|
142
|
+
process.stderr.write(`Error: ${projection.message}\n`);
|
|
143
|
+
process.exit(projection.code);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const findCompletionsCommand = (program: Command): Command | undefined =>
|
|
147
|
+
program.commands.find((command) => command.name() === 'completions');
|
|
148
|
+
|
|
149
|
+
export const attachCompletionsInstallCommand = (
|
|
150
|
+
program: Command,
|
|
151
|
+
options: AttachCompletionsInstallOptions = {}
|
|
152
|
+
): void => {
|
|
153
|
+
const completionsCommand =
|
|
154
|
+
findCompletionsCommand(program) ??
|
|
155
|
+
program
|
|
156
|
+
.command('completions')
|
|
157
|
+
.description('Render and install shell completion scripts');
|
|
158
|
+
|
|
159
|
+
completionsCommand
|
|
160
|
+
.command('install')
|
|
161
|
+
.description('Install a shell completion script for the trails CLI')
|
|
162
|
+
.option(
|
|
163
|
+
'-s, --shell <shell>',
|
|
164
|
+
'Target shell; auto-detected from $SHELL when omitted.'
|
|
165
|
+
)
|
|
166
|
+
.action(async (flags: { readonly shell?: string | undefined }) => {
|
|
167
|
+
const result = await runCompletionsInstall({
|
|
168
|
+
binName: options.binName,
|
|
169
|
+
homeDir: options.homeDir,
|
|
170
|
+
shell: flags.shell,
|
|
171
|
+
shellEnv: options.shellEnv,
|
|
172
|
+
});
|
|
173
|
+
if (result.isErr()) {
|
|
174
|
+
handleCliError(result.error);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
(options.stdout ?? process.stdout).write(`${result.value.message}\n`);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `run.example` trail.
|
|
3
|
+
*
|
|
4
|
+
* `run.example` resolves the named example on the target trail, executes it
|
|
5
|
+
* through the full pipeline, and packages an actual-vs-expected comparison
|
|
6
|
+
* into a structured envelope on the trail's outer `Result.ok(...)`. This module
|
|
7
|
+
* owns the surface decision of how to render that envelope:
|
|
8
|
+
*
|
|
9
|
+
* - Text mode (default): a compact summary on match, an `input / expected /
|
|
10
|
+
* actual / diff` block on mismatch.
|
|
11
|
+
* - JSON / JSONL: emits the full {@link RunExampleComparison} envelope so
|
|
12
|
+
* downstream consumers can parse the comparison shape directly.
|
|
13
|
+
*
|
|
14
|
+
* Match/mismatch is a comparison outcome, not an execution error: the trail
|
|
15
|
+
* always returns `Result.ok(envelope)`. This helper maps mismatch onto a
|
|
16
|
+
* non-zero exit code by throwing a `ValidationError` (category `validation`,
|
|
17
|
+
* exit 1) so Commander's error path runs.
|
|
18
|
+
*
|
|
19
|
+
* Outer Err on the run trail (NotFound, Ambiguous, Validation) is unaffected
|
|
20
|
+
* by `run.example`: this helper defers to the default handler so existing
|
|
21
|
+
* exit-code mapping and recovery hooks stay intact.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
25
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
26
|
+
import { ValidationError } from '@ontrails/core';
|
|
27
|
+
|
|
28
|
+
import { runExampleComparisonSchema } from './trails/run-example.js';
|
|
29
|
+
import type { RunExampleComparison } from './trails/run-example.js';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Detection
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const isExampleRunCtx = (ctx: ActionResultContext): boolean =>
|
|
36
|
+
ctx.trail.id === 'run.example';
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Text formatting
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const formatJson = (value: unknown): string => {
|
|
43
|
+
try {
|
|
44
|
+
const encoded = JSON.stringify(value, null, 2);
|
|
45
|
+
return encoded === undefined ? String(value) : encoded;
|
|
46
|
+
} catch {
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const formatMatchText = (envelope: RunExampleComparison): string =>
|
|
52
|
+
[
|
|
53
|
+
`OK ${envelope.trailId} :: ${envelope.exampleName}`,
|
|
54
|
+
`mode: ${envelope.mode}`,
|
|
55
|
+
'actual matches expected.',
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const formatMismatchText = (envelope: RunExampleComparison): string => {
|
|
59
|
+
const diffBlock =
|
|
60
|
+
envelope.diff !== undefined && envelope.diff.length > 0
|
|
61
|
+
? envelope.diff.map((line) => ` - ${line}`).join('\n')
|
|
62
|
+
: ' - <no diff lines>';
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
`MISMATCH ${envelope.trailId} :: ${envelope.exampleName}`,
|
|
66
|
+
`mode: ${envelope.mode}`,
|
|
67
|
+
'input:',
|
|
68
|
+
formatJson(envelope.input),
|
|
69
|
+
'expected:',
|
|
70
|
+
formatJson(envelope.expected),
|
|
71
|
+
'actual:',
|
|
72
|
+
formatJson(envelope.actual),
|
|
73
|
+
'diff:',
|
|
74
|
+
diffBlock,
|
|
75
|
+
].join('\n');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Entry point
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Return value:
|
|
84
|
+
* - `false` — `run.example` did not apply; caller should fall through to its
|
|
85
|
+
* default handler.
|
|
86
|
+
* - `true` — `run.example` handled the result and wrote output. Caller should
|
|
87
|
+
* not invoke the default handler.
|
|
88
|
+
*
|
|
89
|
+
* Throws a {@link ValidationError} on mismatch so Commander's error path runs
|
|
90
|
+
* and the run trail surface exits with the validation category exit code.
|
|
91
|
+
*/
|
|
92
|
+
export const tryExampleRunOutput = (ctx: ActionResultContext): boolean => {
|
|
93
|
+
if (!isExampleRunCtx(ctx)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Outer Err on the run.example trail (NotFound, Ambiguous, Validation) is not
|
|
98
|
+
// in scope here: defer to the default handler so existing
|
|
99
|
+
// exit-code mapping and recovery hooks stay intact.
|
|
100
|
+
if (ctx.result.isErr()) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const envelope = runExampleComparisonSchema.safeParse(ctx.result.value);
|
|
105
|
+
if (!envelope.success) {
|
|
106
|
+
// Defensive fallback: the trail owns the output schema, so this branch is
|
|
107
|
+
// unreachable in practice. Defer to the default handler if anything else
|
|
108
|
+
// slips through.
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const comparison = envelope.data;
|
|
112
|
+
|
|
113
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
114
|
+
|
|
115
|
+
if (mode === 'text') {
|
|
116
|
+
if (comparison.match) {
|
|
117
|
+
output(formatMatchText(comparison), mode);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
process.stderr.write(`${formatMismatchText(comparison)}\n`);
|
|
121
|
+
throw new ValidationError(
|
|
122
|
+
`Example '${comparison.exampleName}' on trail '${comparison.trailId}' did not match expected outcome.`,
|
|
123
|
+
{
|
|
124
|
+
context: {
|
|
125
|
+
exampleName: comparison.exampleName,
|
|
126
|
+
mode: comparison.mode,
|
|
127
|
+
trailId: comparison.trailId,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// JSON / JSONL: emit the full envelope so downstream consumers can parse
|
|
134
|
+
// the comparison shape directly.
|
|
135
|
+
output(comparison, mode);
|
|
136
|
+
if (!comparison.match) {
|
|
137
|
+
throw new ValidationError(
|
|
138
|
+
`Example '${comparison.exampleName}' on trail '${comparison.trailId}' did not match expected outcome.`,
|
|
139
|
+
{
|
|
140
|
+
context: {
|
|
141
|
+
exampleName: comparison.exampleName,
|
|
142
|
+
mode: comparison.mode,
|
|
143
|
+
trailId: comparison.trailId,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `run.examples` trail.
|
|
3
|
+
*
|
|
4
|
+
* `run.examples` is a pure metadata read: the trail returns a
|
|
5
|
+
* {@link RunExamplesListing} on its outer Ok value.
|
|
6
|
+
* This module owns the surface decision of how to render that listing:
|
|
7
|
+
*
|
|
8
|
+
* - Text mode (default): a table-like list with `name`, truncated `input`,
|
|
9
|
+
* and outcome (`ok` / `error: <code>`). Empty listings print
|
|
10
|
+
* `No examples defined`.
|
|
11
|
+
* - JSON / JSONL: the structured `examples` array is emitted directly via
|
|
12
|
+
* the resolved output mode, so agents and downstream consumers can parse
|
|
13
|
+
* the full structured shape (`name`, `input`, `expected`, `expectedMatch`,
|
|
14
|
+
* `error`, `signals`, `kind`, `provenance`).
|
|
15
|
+
*
|
|
16
|
+
* Errors at the outer layer (NotFoundError, AmbiguousError, ValidationError)
|
|
17
|
+
* are unaffected by `run.examples`: we always defer to the supplied default
|
|
18
|
+
* handler so the existing exit-code mapping and recovery hooks stay intact.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
22
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
23
|
+
import type { StructuredTrailExample } from '@ontrails/core';
|
|
24
|
+
|
|
25
|
+
import { runExamplesListingSchema } from './trails/run-examples.js';
|
|
26
|
+
import type { RunExamplesListing } from './trails/run-examples.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Detection
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const isExamplesRunCtx = (ctx: ActionResultContext): boolean =>
|
|
33
|
+
ctx.trail.id === 'run.examples';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Text formatting
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const INPUT_PREVIEW_LIMIT = 60;
|
|
40
|
+
|
|
41
|
+
const truncate = (value: string, limit: number): string =>
|
|
42
|
+
value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
43
|
+
|
|
44
|
+
const formatInputPreview = (input: unknown): string => {
|
|
45
|
+
if (input === undefined) {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
let encoded: string;
|
|
49
|
+
try {
|
|
50
|
+
encoded = JSON.stringify(input) ?? '';
|
|
51
|
+
} catch {
|
|
52
|
+
encoded = String(input);
|
|
53
|
+
}
|
|
54
|
+
return truncate(encoded, INPUT_PREVIEW_LIMIT);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const formatOutcome = (example: StructuredTrailExample): string => {
|
|
58
|
+
if (example.kind === 'error') {
|
|
59
|
+
const code = example.error;
|
|
60
|
+
return code === undefined || code.length === 0 ? 'error' : `error: ${code}`;
|
|
61
|
+
}
|
|
62
|
+
return 'ok';
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface ExampleRow {
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly input: string;
|
|
68
|
+
readonly outcome: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const buildRows = (
|
|
72
|
+
examples: readonly StructuredTrailExample[]
|
|
73
|
+
): readonly ExampleRow[] =>
|
|
74
|
+
examples.map((example) => ({
|
|
75
|
+
input: formatInputPreview(example.input),
|
|
76
|
+
name: example.name,
|
|
77
|
+
outcome: formatOutcome(example),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const padRight = (value: string, width: number): string =>
|
|
81
|
+
value.length >= width ? value : value + ' '.repeat(width - value.length);
|
|
82
|
+
|
|
83
|
+
const formatTable = (rows: readonly ExampleRow[]): string => {
|
|
84
|
+
const headers = { input: 'INPUT', name: 'NAME', outcome: 'OUTCOME' };
|
|
85
|
+
const allRows: readonly ExampleRow[] = [headers, ...rows];
|
|
86
|
+
|
|
87
|
+
const widths = {
|
|
88
|
+
input: Math.max(...allRows.map((row) => row.input.length)),
|
|
89
|
+
name: Math.max(...allRows.map((row) => row.name.length)),
|
|
90
|
+
outcome: Math.max(...allRows.map((row) => row.outcome.length)),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const formatRow = (row: ExampleRow): string =>
|
|
94
|
+
`${padRight(row.name, widths.name)} ${padRight(row.input, widths.input)} ${row.outcome}`;
|
|
95
|
+
|
|
96
|
+
return allRows.map(formatRow).join('\n');
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const formatTextListing = (listing: RunExamplesListing): string => {
|
|
100
|
+
if (listing.examples.length === 0) {
|
|
101
|
+
return 'No examples defined';
|
|
102
|
+
}
|
|
103
|
+
return formatTable(buildRows(listing.examples));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Entry point
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Return value:
|
|
112
|
+
* - `false` — `run.examples` did not apply; caller should fall through to its
|
|
113
|
+
* default handler.
|
|
114
|
+
* - `true` — `run.examples` handled the result and wrote output. Caller should
|
|
115
|
+
* not invoke the default handler.
|
|
116
|
+
*/
|
|
117
|
+
export const tryExamplesRunOutput = (ctx: ActionResultContext): boolean => {
|
|
118
|
+
if (!isExamplesRunCtx(ctx)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Outer Err on `run.examples` (NotFound, Ambiguous, Validation) is not in
|
|
123
|
+
// scope for this renderer: defer to the default handler so existing
|
|
124
|
+
// exit-code mapping and recovery hooks stay intact.
|
|
125
|
+
if (ctx.result.isErr()) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const listing = runExamplesListingSchema.safeParse(ctx.result.value);
|
|
130
|
+
if (!listing.success) {
|
|
131
|
+
// Defensive fallback: the trail owns the output schema, so this branch is
|
|
132
|
+
// unreachable in practice. Defer to the default handler if anything else
|
|
133
|
+
// slips through.
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
138
|
+
|
|
139
|
+
if (mode === 'text') {
|
|
140
|
+
output(formatTextListing(listing.data), mode);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// JSON / JSONL: emit the structured examples array directly so downstream
|
|
145
|
+
// consumers can parse the full structured shape per example.
|
|
146
|
+
output(listing.data.examples, mode);
|
|
147
|
+
return true;
|
|
148
|
+
};
|
package/src/run-quiet.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-surface bridge for the `run` trail's `--quiet` flag.
|
|
3
|
+
*
|
|
4
|
+
* The `run` trail returns an `inner-trail-result` envelope as the `value` of
|
|
5
|
+
* its own outer `Result.ok(...)`. The default CLI on-result handler unwraps
|
|
6
|
+
* once, leaving stdout as `{ "kind": "inner-trail-result", "trailId": "...",
|
|
7
|
+
* "value": ... }` — useful when a caller wants provenance, but noisy when the
|
|
8
|
+
* caller only wants the inner value to feed downstream pipes.
|
|
9
|
+
*
|
|
10
|
+
* `--quiet` strips that envelope:
|
|
11
|
+
*
|
|
12
|
+
* - Inner result envelope → write the inner value to stdout via the resolved
|
|
13
|
+
* output mode.
|
|
14
|
+
*
|
|
15
|
+
* Errors at the outer layer (the run trail itself failing — `NotFoundError`,
|
|
16
|
+
* `AmbiguousError`, `ValidationError` from input) are unaffected by `--quiet`:
|
|
17
|
+
* we always defer to the supplied default handler so collision-recovery and
|
|
18
|
+
* the existing error surface stay intact.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
22
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
23
|
+
|
|
24
|
+
import { INNER_TRAIL_RESULT_KIND } from './trails/run.js';
|
|
25
|
+
|
|
26
|
+
interface InnerTrailResultEnvelope {
|
|
27
|
+
readonly kind: typeof INNER_TRAIL_RESULT_KIND;
|
|
28
|
+
readonly trailId: string;
|
|
29
|
+
readonly value: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isInnerTrailResultEnvelope = (
|
|
33
|
+
value: unknown
|
|
34
|
+
): value is InnerTrailResultEnvelope =>
|
|
35
|
+
typeof value === 'object' &&
|
|
36
|
+
value !== null &&
|
|
37
|
+
(value as { readonly kind?: unknown }).kind === INNER_TRAIL_RESULT_KIND &&
|
|
38
|
+
typeof (value as { readonly trailId?: unknown }).trailId === 'string' &&
|
|
39
|
+
'value' in value;
|
|
40
|
+
|
|
41
|
+
const isQuietRunCtx = (ctx: ActionResultContext): boolean =>
|
|
42
|
+
ctx.trail.id === 'run' && ctx.flags['quiet'] === true;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Return value:
|
|
46
|
+
* - `false` — `--quiet` did not apply; caller should fall through to its
|
|
47
|
+
* default handler.
|
|
48
|
+
* - `true` — `--quiet` handled the result and wrote output. Caller should not
|
|
49
|
+
* invoke the default handler.
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
export const tryQuietRunOutput = async (
|
|
53
|
+
ctx: ActionResultContext
|
|
54
|
+
): Promise<boolean> => {
|
|
55
|
+
if (!isQuietRunCtx(ctx)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Outer Err on the run trail (collision, not-found, validation) is not in
|
|
60
|
+
// scope for --quiet: defer to the default handler so existing exit-code
|
|
61
|
+
// mapping and recovery hooks stay intact.
|
|
62
|
+
if (ctx.result.isErr()) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const inner: unknown = ctx.result.value;
|
|
67
|
+
if (!isInnerTrailResultEnvelope(inner)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
72
|
+
const value = inner.value === undefined ? null : inner.value;
|
|
73
|
+
output(value, mode);
|
|
74
|
+
return true;
|
|
75
|
+
};
|