@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
package/src/cli.ts
CHANGED
|
@@ -1,14 +1,308 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
defaultOnResult,
|
|
7
|
+
devPermitPreset,
|
|
8
|
+
outputModePreset,
|
|
9
|
+
permitPreset,
|
|
10
|
+
tokenPreset,
|
|
11
|
+
tracePreset,
|
|
12
|
+
watchPreset,
|
|
13
|
+
} from '@ontrails/cli';
|
|
14
|
+
import type {
|
|
15
|
+
ActionResultContext,
|
|
16
|
+
ResolveCliPermitFromToken,
|
|
17
|
+
} from '@ontrails/cli';
|
|
18
|
+
import { createProgram } from '@ontrails/commander';
|
|
19
|
+
import { resolvePermitFromBearerToken } from '@ontrails/permits';
|
|
20
|
+
import { deriveTopoGraph } from '@ontrails/topographer';
|
|
3
21
|
|
|
4
22
|
import { app } from './app.js';
|
|
5
23
|
import { resolveInputWithClack } from './clack.js';
|
|
24
|
+
import { attachCompletionsInstallCommand } from './run-completions-install.js';
|
|
25
|
+
import { tryRecoverFromRunCollision } from './run-collision.js';
|
|
26
|
+
import { tryExampleRunOutput } from './run-example.js';
|
|
27
|
+
import { tryExamplesRunOutput } from './run-examples.js';
|
|
28
|
+
import { tryQuietRunOutput } from './run-quiet.js';
|
|
29
|
+
import {
|
|
30
|
+
argvHasTraceFlag,
|
|
31
|
+
installTraceSink,
|
|
32
|
+
tryTraceJsonOutput,
|
|
33
|
+
writeTraceTreeToStderr,
|
|
34
|
+
} from './run-trace.js';
|
|
35
|
+
import type { TraceSession } from './run-trace.js';
|
|
36
|
+
import {
|
|
37
|
+
argvHasWatchFlag,
|
|
38
|
+
hashTopoGraphEntry,
|
|
39
|
+
readRunTrailId,
|
|
40
|
+
runWatchLoop,
|
|
41
|
+
} from './run-watch.js';
|
|
42
|
+
import { tryWardenOutput } from './run-warden.js';
|
|
43
|
+
import { tryLoadFreshAppLease } from './trails/load-app.js';
|
|
44
|
+
import { resolveRunModulePath } from './trails/run.js';
|
|
45
|
+
import { resolveTrailRootDir } from './trails/root-dir.js';
|
|
46
|
+
import { trailsPackageVersion } from './versions.js';
|
|
47
|
+
|
|
48
|
+
const buildOnResult =
|
|
49
|
+
(session: TraceSession | undefined) =>
|
|
50
|
+
async (ctx: ActionResultContext): Promise<void> => {
|
|
51
|
+
const recovered = await tryRecoverFromRunCollision(ctx, { graph: app });
|
|
52
|
+
const resolvedCtx: ActionResultContext =
|
|
53
|
+
recovered === undefined
|
|
54
|
+
? ctx
|
|
55
|
+
: {
|
|
56
|
+
...ctx,
|
|
57
|
+
input: recovered.isOk()
|
|
58
|
+
? (ctx.trail.input.safeParse(ctx.input).data ?? ctx.input)
|
|
59
|
+
: ctx.input,
|
|
60
|
+
result: recovered,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// `--trace --json` (without `--quiet`) emits a single Result-shaped
|
|
64
|
+
// envelope on stdout that includes the captured records under
|
|
65
|
+
// `tracing`. Hand that case off before the regular chain so the
|
|
66
|
+
// existing handlers do not also write to stdout.
|
|
67
|
+
if (session !== undefined && tryTraceJsonOutput(resolvedCtx, session)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (tryExampleRunOutput(resolvedCtx)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (tryExamplesRunOutput(resolvedCtx)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (await tryQuietRunOutput(resolvedCtx)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (tryWardenOutput(resolvedCtx)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await defaultOnResult(resolvedCtx);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const traceEnabled = argvHasTraceFlag(process.argv);
|
|
87
|
+
const maybeInstallTraceSession = (): TraceSession | undefined =>
|
|
88
|
+
traceEnabled ? installTraceSink() : undefined;
|
|
89
|
+
|
|
90
|
+
const resolveCliPermitFromToken: ResolveCliPermitFromToken = (input) =>
|
|
91
|
+
resolvePermitFromBearerToken({
|
|
92
|
+
bearerToken: input.token,
|
|
93
|
+
configValues: input.configValues,
|
|
94
|
+
env: process.env as Record<string, string | undefined>,
|
|
95
|
+
graph: input.graph,
|
|
96
|
+
missingAuthResourceMessage:
|
|
97
|
+
'--token requires an auth adapter. Register authResource from @ontrails/permits in your topo.',
|
|
98
|
+
nullPermitMessage: 'Auth adapter did not produce a permit for --token',
|
|
99
|
+
requestId: input.requestId,
|
|
100
|
+
resources: input.resources,
|
|
101
|
+
surface: 'cli',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
interface WatchRunTarget {
|
|
105
|
+
readonly app?: string | undefined;
|
|
106
|
+
readonly id: string;
|
|
107
|
+
readonly module?: string | undefined;
|
|
108
|
+
readonly rootDir?: string | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const readFlagValue = (
|
|
112
|
+
args: readonly string[],
|
|
113
|
+
flagName: string
|
|
114
|
+
): string | undefined => {
|
|
115
|
+
const longFlag = `--${flagName}`;
|
|
116
|
+
const prefixedFlag = `${longFlag}=`;
|
|
117
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
118
|
+
const arg = args[i];
|
|
119
|
+
if (arg === longFlag) {
|
|
120
|
+
return args[i + 1];
|
|
121
|
+
}
|
|
122
|
+
if (arg?.startsWith(prefixedFlag)) {
|
|
123
|
+
return arg.slice(prefixedFlag.length);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const resolveWatchRunTarget = (
|
|
130
|
+
argv: readonly string[]
|
|
131
|
+
): WatchRunTarget | null => {
|
|
132
|
+
const args = argv.slice(2);
|
|
133
|
+
const id = readRunTrailId(args);
|
|
134
|
+
if (id === undefined) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
app: readFlagValue(args, 'app'),
|
|
139
|
+
id,
|
|
140
|
+
module: readFlagValue(args, 'module'),
|
|
141
|
+
rootDir: readFlagValue(args, 'root-dir'),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the directory whose source-file events wake the `--watch` loop.
|
|
147
|
+
* Reruns still depend on the resolved TopoGraph entry hash; this path is
|
|
148
|
+
* only the cheap filesystem event source.
|
|
149
|
+
*/
|
|
150
|
+
const toWatchSourcePath = (rootDir: string, modulePath: string): string => {
|
|
151
|
+
if (modulePath.startsWith('file:')) {
|
|
152
|
+
return fileURLToPath(modulePath);
|
|
153
|
+
}
|
|
154
|
+
return isAbsolute(modulePath) ? modulePath : resolve(rootDir, modulePath);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const resolveWatchDirectorySourcePath = async (
|
|
158
|
+
target: WatchRunTarget | null
|
|
159
|
+
): Promise<string> => {
|
|
160
|
+
if (target !== null) {
|
|
161
|
+
const rootDirResult = resolveTrailRootDir(target.rootDir, process.cwd());
|
|
162
|
+
if (rootDirResult.isErr()) {
|
|
163
|
+
throw rootDirResult.error;
|
|
164
|
+
}
|
|
165
|
+
const moduleResult = await resolveRunModulePath(
|
|
166
|
+
rootDirResult.value,
|
|
167
|
+
target.module,
|
|
168
|
+
target.id,
|
|
169
|
+
target.app
|
|
170
|
+
);
|
|
171
|
+
if (moduleResult.isOk()) {
|
|
172
|
+
return toWatchSourcePath(rootDirResult.value, moduleResult.value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const cwd = process.cwd();
|
|
176
|
+
const srcDir = join(cwd, 'src');
|
|
177
|
+
if (existsSync(srcDir)) {
|
|
178
|
+
return join(srcDir, 'app.ts');
|
|
179
|
+
}
|
|
180
|
+
return join(cwd, 'app.ts');
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const readWatchTopoGraphEntryHash = async (
|
|
184
|
+
target: WatchRunTarget | null
|
|
185
|
+
): Promise<string | null> => {
|
|
186
|
+
if (target === null) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const rootDirResult = resolveTrailRootDir(target.rootDir, process.cwd());
|
|
190
|
+
if (rootDirResult.isErr()) {
|
|
191
|
+
throw rootDirResult.error;
|
|
192
|
+
}
|
|
193
|
+
const rootDir = rootDirResult.value;
|
|
194
|
+
const moduleResult = await resolveRunModulePath(
|
|
195
|
+
rootDir,
|
|
196
|
+
target.module,
|
|
197
|
+
target.id,
|
|
198
|
+
target.app
|
|
199
|
+
);
|
|
200
|
+
if (moduleResult.isErr()) {
|
|
201
|
+
throw moduleResult.error;
|
|
202
|
+
}
|
|
203
|
+
const leaseResult = await tryLoadFreshAppLease(moduleResult.value, rootDir);
|
|
204
|
+
if (leaseResult.isErr()) {
|
|
205
|
+
throw leaseResult.error;
|
|
206
|
+
}
|
|
207
|
+
const lease = leaseResult.value;
|
|
208
|
+
try {
|
|
209
|
+
const topoGraph = deriveTopoGraph(lease.app);
|
|
210
|
+
const entry = topoGraph.entries.find(
|
|
211
|
+
(candidate) => candidate.kind === 'trail' && candidate.id === target.id
|
|
212
|
+
);
|
|
213
|
+
return entry === undefined ? null : hashTopoGraphEntry(entry);
|
|
214
|
+
} finally {
|
|
215
|
+
lease.release();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const wardenValueFlags = new Set([
|
|
220
|
+
'--apps',
|
|
221
|
+
'--config-path',
|
|
222
|
+
'--depth',
|
|
223
|
+
'--drafts',
|
|
224
|
+
'--fail-on',
|
|
225
|
+
'--format',
|
|
226
|
+
'--lock',
|
|
227
|
+
'--root-dir',
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
const normalizeWardenArgv = (argv: readonly string[]): string[] => {
|
|
231
|
+
if (argv[2] !== 'warden') {
|
|
232
|
+
return [...argv];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const normalized = [...argv];
|
|
236
|
+
let previousFlagConsumesValue = false;
|
|
237
|
+
for (let index = 3; index < normalized.length; index += 1) {
|
|
238
|
+
const arg = normalized[index];
|
|
239
|
+
if (arg === undefined) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (previousFlagConsumesValue) {
|
|
244
|
+
previousFlagConsumesValue = false;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (arg === '-a') {
|
|
249
|
+
normalized[index] = '--apps';
|
|
250
|
+
previousFlagConsumesValue = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
previousFlagConsumesValue = wardenValueFlags.has(arg);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return normalized;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Invoke `surface()` once with an optional fresh trace session.
|
|
262
|
+
*
|
|
263
|
+
* When `--trace` is set, a fresh {@link TraceSession} is installed for the
|
|
264
|
+
* duration of the call and finalized in the `finally` block. Under
|
|
265
|
+
* `--watch`, this produces a fresh sink (and a fresh stderr tree) per
|
|
266
|
+
* rerun rather than letting records accumulate in a single
|
|
267
|
+
* process-lifetime sink.
|
|
268
|
+
*/
|
|
269
|
+
const runSurfaceOnce = async (): Promise<void> => {
|
|
270
|
+
const session = maybeInstallTraceSession();
|
|
271
|
+
try {
|
|
272
|
+
const program = createProgram(app, {
|
|
273
|
+
description: 'Agent-native, contract-first TypeScript framework',
|
|
274
|
+
name: 'trails',
|
|
275
|
+
onResult: buildOnResult(session),
|
|
276
|
+
presets: [
|
|
277
|
+
outputModePreset(),
|
|
278
|
+
tracePreset(),
|
|
279
|
+
permitPreset(),
|
|
280
|
+
tokenPreset(),
|
|
281
|
+
devPermitPreset(),
|
|
282
|
+
watchPreset(),
|
|
283
|
+
],
|
|
284
|
+
resolveInput: resolveInputWithClack,
|
|
285
|
+
resolvePermitFromToken: resolveCliPermitFromToken,
|
|
286
|
+
version: trailsPackageVersion,
|
|
287
|
+
});
|
|
288
|
+
attachCompletionsInstallCommand(program);
|
|
289
|
+
await program.parseAsync(normalizeWardenArgv(process.argv));
|
|
290
|
+
} finally {
|
|
291
|
+
if (session !== undefined) {
|
|
292
|
+
const records = session.finalize();
|
|
293
|
+
writeTraceTreeToStderr(records);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const watchTarget = argvHasWatchFlag(process.argv)
|
|
299
|
+
? resolveWatchRunTarget(process.argv)
|
|
300
|
+
: null;
|
|
6
301
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
302
|
+
await (argvHasWatchFlag(process.argv)
|
|
303
|
+
? runWatchLoop({
|
|
304
|
+
readTopoGraphEntryHash: () => readWatchTopoGraphEntryHash(watchTarget),
|
|
305
|
+
run: runSurfaceOnce,
|
|
306
|
+
sourcePath: await resolveWatchDirectorySourcePath(watchTarget),
|
|
307
|
+
})
|
|
308
|
+
: runSurfaceOnce());
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion infrastructure for the `trails` CLI.
|
|
3
|
+
*
|
|
4
|
+
* The completion model is a two-part system:
|
|
5
|
+
*
|
|
6
|
+
* 1. A small, static **shell script** registered with the user's shell. The
|
|
7
|
+
* script's only job is to invoke the binary's internal completion subcommand
|
|
8
|
+
* (`trails completions __complete`) with the partial argv at tab-press time
|
|
9
|
+
* and feed the resulting lines back to the shell.
|
|
10
|
+
* 2. A dynamic **`__complete` trail** that parses the partial argv and emits
|
|
11
|
+
* a sorted list of suggestions (e.g. trail IDs).
|
|
12
|
+
*
|
|
13
|
+
* This split keeps the shell-side script tiny and standardish (no rich shell
|
|
14
|
+
* DSL), while the heavy lifting stays in TypeScript where it can reuse the
|
|
15
|
+
* workspace trail index for accurate, live suggestions.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
deriveStructuredTrailExamples,
|
|
20
|
+
RecoverableCompletionError,
|
|
21
|
+
Result,
|
|
22
|
+
ValidationError,
|
|
23
|
+
} from '@ontrails/core';
|
|
24
|
+
import { buildWorkspaceTrailIndex } from '@ontrails/topographer';
|
|
25
|
+
|
|
26
|
+
import { tryLoadFreshAppLease } from './trails/load-app.js';
|
|
27
|
+
|
|
28
|
+
/** Shells supported by the completion generator. */
|
|
29
|
+
export type CompletionShell = 'bash' | 'zsh' | 'fish';
|
|
30
|
+
|
|
31
|
+
type ScriptRenderer = (binName: string) => string;
|
|
32
|
+
|
|
33
|
+
const renderBashScript: ScriptRenderer = (binName) =>
|
|
34
|
+
`# ${binName} bash completion
|
|
35
|
+
_${binName}_complete() {
|
|
36
|
+
local cur words
|
|
37
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
38
|
+
words=("\${COMP_WORDS[@]:1:COMP_CWORD}")
|
|
39
|
+
COMPREPLY=()
|
|
40
|
+
while IFS= read -r suggestion; do
|
|
41
|
+
COMPREPLY+=("$suggestion")
|
|
42
|
+
done < <(${binName} completions __complete "\${words[@]}" 2>/dev/null)
|
|
43
|
+
return 0
|
|
44
|
+
}
|
|
45
|
+
complete -F _${binName}_complete ${binName}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const renderZshScript: ScriptRenderer = (binName) =>
|
|
49
|
+
`#compdef ${binName}
|
|
50
|
+
# ${binName} zsh completion
|
|
51
|
+
_${binName}_complete() {
|
|
52
|
+
local -a suggestions trail_words
|
|
53
|
+
local output
|
|
54
|
+
trail_words=("\${(@)words[2,CURRENT]}")
|
|
55
|
+
output="$(${binName} completions __complete "\${trail_words[@]}" 2>/dev/null)"
|
|
56
|
+
if [[ -n "$output" ]]; then
|
|
57
|
+
suggestions=("\${(@f)output}")
|
|
58
|
+
if (( \${#suggestions} )); then
|
|
59
|
+
compadd -- "\${suggestions[@]}"
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
}
|
|
63
|
+
compdef _${binName}_complete ${binName}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const renderFishScript: ScriptRenderer = (binName) =>
|
|
67
|
+
`# ${binName} fish completion
|
|
68
|
+
function __${binName}_complete
|
|
69
|
+
set -l tokens (commandline -opc) (commandline -ct)
|
|
70
|
+
set -e tokens[1]
|
|
71
|
+
${binName} completions __complete $tokens 2>/dev/null
|
|
72
|
+
end
|
|
73
|
+
complete -c ${binName} -f -a '(__${binName}_complete)'
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const SCRIPT_RENDERERS: Readonly<Record<CompletionShell, ScriptRenderer>> = {
|
|
77
|
+
bash: renderBashScript,
|
|
78
|
+
fish: renderFishScript,
|
|
79
|
+
zsh: renderZshScript,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Pattern that `binName` must match — alphanumerics, underscore, hyphen. */
|
|
83
|
+
const BIN_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
84
|
+
|
|
85
|
+
const recoverableCompletionError = (
|
|
86
|
+
message: string,
|
|
87
|
+
context: Record<string, unknown>,
|
|
88
|
+
cause?: unknown
|
|
89
|
+
): RecoverableCompletionError =>
|
|
90
|
+
new RecoverableCompletionError(message, {
|
|
91
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
92
|
+
context,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render a static shell completion script that delegates dynamic completion
|
|
97
|
+
* to `<binName> completions __complete <args...>`.
|
|
98
|
+
*
|
|
99
|
+
* @param shell - target shell flavor.
|
|
100
|
+
* @param binName - binary name to register the completion against (typically
|
|
101
|
+
* `'trails'`). Used both as the registered command and as the prefix for the
|
|
102
|
+
* shell function name. Must match `/^[a-zA-Z0-9_-]+$/` — the value is
|
|
103
|
+
* interpolated verbatim into shell source, so any non-trivial input would
|
|
104
|
+
* be a shell-injection vector. We validate at the boundary per
|
|
105
|
+
* "validate at the boundary, trust internally" (docs/tenets.md).
|
|
106
|
+
*/
|
|
107
|
+
export const renderCompletionScript = (
|
|
108
|
+
shell: CompletionShell,
|
|
109
|
+
binName: string
|
|
110
|
+
): Result<string, ValidationError> => {
|
|
111
|
+
if (!BIN_NAME_PATTERN.test(binName)) {
|
|
112
|
+
return Result.err(
|
|
113
|
+
new ValidationError(
|
|
114
|
+
`renderCompletionScript: binName must match /^[a-zA-Z0-9_-]+$/ (got: ${JSON.stringify(binName)})`
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return Result.ok(SCRIPT_RENDERERS[shell](binName));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read trail IDs from the live workspace topo and return those matching
|
|
123
|
+
* `prefix`, sorted lexicographically. Includes IDs that collide across multiple
|
|
124
|
+
* apps — the shell only needs the unique set of identifiers, not their owners.
|
|
125
|
+
*
|
|
126
|
+
* @param workspaceRoot - workspace root directory used to resolve apps.
|
|
127
|
+
* @param prefix - prefix to filter by; an empty prefix returns every ID.
|
|
128
|
+
*/
|
|
129
|
+
export const renderTrailIdCompletions = async (
|
|
130
|
+
workspaceRoot: string,
|
|
131
|
+
prefix: string
|
|
132
|
+
): Promise<readonly string[]> => {
|
|
133
|
+
let result: Awaited<ReturnType<typeof buildWorkspaceTrailIndex>>;
|
|
134
|
+
try {
|
|
135
|
+
result = await buildWorkspaceTrailIndex({ cwd: workspaceRoot });
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const ids = new Set<string>(Object.keys(result.index));
|
|
140
|
+
for (const collision of result.collisions) {
|
|
141
|
+
ids.add(collision.trailId);
|
|
142
|
+
}
|
|
143
|
+
const matching: string[] = [];
|
|
144
|
+
for (const id of ids) {
|
|
145
|
+
if (id.startsWith(prefix)) {
|
|
146
|
+
matching.push(id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
matching.sort((a, b) => {
|
|
150
|
+
if (a < b) {
|
|
151
|
+
return -1;
|
|
152
|
+
}
|
|
153
|
+
if (a > b) {
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
});
|
|
158
|
+
return matching;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Example name completion
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Return example names for `trailId` matching `prefix`, sorted lexicographically.
|
|
167
|
+
*
|
|
168
|
+
* Looks the trail up via the workspace index (TRL-404), resolves its owning
|
|
169
|
+
* app module from the enriched index, loads the app's topo, and reads the
|
|
170
|
+
* `name` of every structured example.
|
|
171
|
+
*
|
|
172
|
+
* Completion is best-effort for shell callers, but this helper preserves
|
|
173
|
+
* load-time failures as `RecoverableCompletionError` so the internal bridge can
|
|
174
|
+
* decide whether to suppress them for prompt safety.
|
|
175
|
+
*/
|
|
176
|
+
export const renderTrailExampleCompletions = async (
|
|
177
|
+
workspaceRoot: string,
|
|
178
|
+
trailId: string,
|
|
179
|
+
prefix: string
|
|
180
|
+
): Promise<Result<readonly string[], RecoverableCompletionError>> => {
|
|
181
|
+
try {
|
|
182
|
+
const { index } = await buildWorkspaceTrailIndex({ cwd: workspaceRoot });
|
|
183
|
+
const owner = index[trailId];
|
|
184
|
+
if (owner === undefined) {
|
|
185
|
+
return Result.ok([]);
|
|
186
|
+
}
|
|
187
|
+
const leaseResult = await tryLoadFreshAppLease(
|
|
188
|
+
owner.modulePath,
|
|
189
|
+
workspaceRoot
|
|
190
|
+
);
|
|
191
|
+
if (leaseResult.isErr()) {
|
|
192
|
+
return Result.err(
|
|
193
|
+
recoverableCompletionError(
|
|
194
|
+
'Cannot load app while completing example names',
|
|
195
|
+
{ modulePath: owner.modulePath, trailId, workspaceRoot },
|
|
196
|
+
leaseResult.error
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const lease = leaseResult.value;
|
|
201
|
+
try {
|
|
202
|
+
const target = lease.app.get(trailId);
|
|
203
|
+
if (target === undefined) {
|
|
204
|
+
return Result.err(
|
|
205
|
+
recoverableCompletionError(
|
|
206
|
+
'Indexed trail was not found in loaded app while completing example names',
|
|
207
|
+
{ modulePath: owner.modulePath, trailId, workspaceRoot }
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const structured = deriveStructuredTrailExamples(target.examples) ?? [];
|
|
212
|
+
const matching: string[] = [];
|
|
213
|
+
for (const example of structured) {
|
|
214
|
+
if (example.name.startsWith(prefix)) {
|
|
215
|
+
matching.push(example.name);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
matching.sort((a, b) => {
|
|
219
|
+
if (a < b) {
|
|
220
|
+
return -1;
|
|
221
|
+
}
|
|
222
|
+
if (a > b) {
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
});
|
|
227
|
+
return Result.ok(matching);
|
|
228
|
+
} finally {
|
|
229
|
+
lease.release();
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return Result.err(
|
|
233
|
+
recoverableCompletionError(
|
|
234
|
+
'Cannot resolve workspace while completing example names',
|
|
235
|
+
{ trailId, workspaceRoot },
|
|
236
|
+
error
|
|
237
|
+
)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
basename,
|
|
4
|
+
dirname,
|
|
5
|
+
isAbsolute,
|
|
6
|
+
join,
|
|
7
|
+
parse as parsePath,
|
|
8
|
+
relative,
|
|
9
|
+
resolve,
|
|
10
|
+
} from 'node:path';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
deriveSafePath,
|
|
14
|
+
InternalError,
|
|
15
|
+
PermissionError,
|
|
16
|
+
Result,
|
|
17
|
+
ValidationError,
|
|
18
|
+
} from '@ontrails/core';
|
|
19
|
+
// Result is imported as a value for factories above; this alias keeps returned
|
|
20
|
+
// Result types readable without colliding with the value import.
|
|
21
|
+
import type { Result as TrailsResult } from '@ontrails/core';
|
|
22
|
+
|
|
23
|
+
export const LOAD_APP_MIRROR_PARENT_DIRNAME = '.trails-tmp';
|
|
24
|
+
|
|
25
|
+
export const LOAD_APP_MIRROR_ENTRY_PREFIX = 'load-app-fresh-';
|
|
26
|
+
|
|
27
|
+
const asError = (error: unknown): Error =>
|
|
28
|
+
error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
|
|
30
|
+
const validateMirrorRoot = (
|
|
31
|
+
mirrorRoot: string
|
|
32
|
+
): TrailsResult<string, PermissionError> => {
|
|
33
|
+
const resolved = resolve(mirrorRoot);
|
|
34
|
+
const mirrorParent = dirname(resolved);
|
|
35
|
+
|
|
36
|
+
return basename(mirrorParent) === LOAD_APP_MIRROR_PARENT_DIRNAME &&
|
|
37
|
+
basename(resolved).startsWith(LOAD_APP_MIRROR_ENTRY_PREFIX)
|
|
38
|
+
? Result.ok(resolved)
|
|
39
|
+
: Result.err(
|
|
40
|
+
new PermissionError(
|
|
41
|
+
`Refusing to write or remove non-load-app mirror path "${mirrorRoot}"`,
|
|
42
|
+
{ context: { mirrorRoot: resolved } }
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const resolveAbsoluteSourcePath = (
|
|
48
|
+
sourcePath: string
|
|
49
|
+
): TrailsResult<string, ValidationError> =>
|
|
50
|
+
isAbsolute(sourcePath)
|
|
51
|
+
? Result.ok(sourcePath)
|
|
52
|
+
: Result.err(
|
|
53
|
+
new ValidationError(
|
|
54
|
+
`Load-app mirror source path must be absolute: "${sourcePath}"`,
|
|
55
|
+
{ context: { sourcePath } }
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert an absolute source path to the deterministic location inside a
|
|
61
|
+
* load-app fresh mirror.
|
|
62
|
+
*/
|
|
63
|
+
export const resolveLoadAppMirrorFilePath = (
|
|
64
|
+
sourcePath: string,
|
|
65
|
+
mirrorRoot: string
|
|
66
|
+
): TrailsResult<string, Error> => {
|
|
67
|
+
const root = validateMirrorRoot(mirrorRoot);
|
|
68
|
+
if (root.isErr()) {
|
|
69
|
+
return root;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const source = resolveAbsoluteSourcePath(sourcePath);
|
|
73
|
+
if (source.isErr()) {
|
|
74
|
+
return source;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mirrorRelativePath = relative(
|
|
78
|
+
parsePath(source.value).root,
|
|
79
|
+
source.value
|
|
80
|
+
);
|
|
81
|
+
return deriveSafePath(root.value, mirrorRelativePath);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Copy a source file into its load-app fresh mirror by raw bytes.
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* Reading via `.bytes()` rather than `.text()` preserves binary payloads
|
|
89
|
+
* (`.wasm`, `.node`, compiled assets) that may sit alongside source files in
|
|
90
|
+
* the app's graph. Text decoding would corrupt them on the way through the
|
|
91
|
+
* mirror.
|
|
92
|
+
*/
|
|
93
|
+
export const writeLoadAppMirrorFile = async (
|
|
94
|
+
sourcePath: string,
|
|
95
|
+
mirrorRoot: string
|
|
96
|
+
): Promise<TrailsResult<string, Error>> => {
|
|
97
|
+
const mirrorPath = resolveLoadAppMirrorFilePath(sourcePath, mirrorRoot);
|
|
98
|
+
if (mirrorPath.isErr()) {
|
|
99
|
+
return mirrorPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(dirname(mirrorPath.value), { recursive: true });
|
|
104
|
+
const bytes = await Bun.file(sourcePath).bytes();
|
|
105
|
+
await Bun.write(mirrorPath.value, bytes);
|
|
106
|
+
return Result.ok(mirrorPath.value);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return Result.err(
|
|
109
|
+
new InternalError(`Failed to mirror load-app file "${sourcePath}"`, {
|
|
110
|
+
cause: asError(error),
|
|
111
|
+
context: { mirrorPath: mirrorPath.value, mirrorRoot, sourcePath },
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const removeLoadAppMirrorRoot = (
|
|
118
|
+
mirrorRoot: string
|
|
119
|
+
): TrailsResult<void, Error> => {
|
|
120
|
+
const root = validateMirrorRoot(mirrorRoot);
|
|
121
|
+
if (root.isErr()) {
|
|
122
|
+
return root;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
rmSync(root.value, { force: true, recursive: true });
|
|
127
|
+
return Result.ok();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return Result.err(
|
|
130
|
+
new InternalError(`Failed to remove load-app mirror "${mirrorRoot}"`, {
|
|
131
|
+
cause: asError(error),
|
|
132
|
+
context: { mirrorRoot: root.value },
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Best-effort cleanup for process-exit and stale-sweep paths.
|
|
140
|
+
*
|
|
141
|
+
* This intentionally suppresses validation and filesystem failures because the
|
|
142
|
+
* caller is already abandoning a temporary mirror and cleanup must not turn
|
|
143
|
+
* into an application-load failure.
|
|
144
|
+
*/
|
|
145
|
+
export const removeLoadAppMirrorRootQuietly = (mirrorRoot: string): void => {
|
|
146
|
+
try {
|
|
147
|
+
removeLoadAppMirrorRoot(mirrorRoot);
|
|
148
|
+
} catch {
|
|
149
|
+
// Best-effort cleanup must never become the failure path.
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const createLoadAppMirrorRootPath = (cwd: string): string =>
|
|
154
|
+
join(
|
|
155
|
+
resolve(cwd),
|
|
156
|
+
LOAD_APP_MIRROR_PARENT_DIRNAME,
|
|
157
|
+
`${LOAD_APP_MIRROR_ENTRY_PREFIX}${Date.now()}-${Math.random()
|
|
158
|
+
.toString(36)
|
|
159
|
+
.slice(2)}`
|
|
160
|
+
);
|