@ontrails/trails 1.0.0-beta.4 → 1.0.0-beta.42
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 +1106 -4
- package/README.md +27 -0
- package/package.json +31 -7
- package/src/app.ts +147 -3
- package/src/cli.ts +345 -11
- package/src/completions.ts +240 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +173 -0
- package/src/mcp-app.ts +42 -0
- package/src/mcp-options.ts +91 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +430 -0
- package/src/regrade/live-api-preserve.ts +8 -0
- package/src/regrade/plan-artifact.ts +285 -0
- package/src/release/bindings.ts +39 -0
- package/src/release/check.ts +844 -0
- package/src/release/config.ts +63 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/index.ts +142 -0
- package/src/release/lock-roundtrip-smoke.ts +230 -0
- package/src/release/native-bun-publish.ts +651 -0
- package/src/release/native-bun-registry.ts +765 -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/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1687 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/wayfinder-dogfood-smoke.ts +760 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -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-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.ts +432 -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 +99 -45
- package/src/trails/add-trail.ts +84 -37
- package/src/trails/add-verify.ts +100 -30
- package/src/trails/compile.ts +58 -0
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +401 -106
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +186 -72
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +82 -0
- package/src/trails/dev-reset.ts +50 -0
- package/src/trails/dev-stats.ts +72 -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 +70 -68
- package/src/trails/load-app.ts +1123 -15
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project.ts +17 -3
- package/src/trails/regrade.ts +3091 -0
- package/src/trails/release-check.ts +105 -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 +475 -0
- package/src/trails/run-examples.ts +129 -0
- package/src/trails/run.ts +434 -0
- package/src/trails/scaffold-json.ts +58 -0
- package/src/trails/survey.ts +877 -214
- 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 +325 -0
- package/src/trails/topo-support.ts +220 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +27 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +198 -58
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1053 -0
- package/src/versions.ts +31 -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 -6
- 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 -11
- 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-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/guide.d.ts +0 -11
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -80
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -4
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -24
- 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 -43
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -31
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -221
- package/dist/src/trails/survey.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 -88
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/create.test.ts +0 -349
- package/src/__tests__/guide.test.ts +0 -91
- package/src/__tests__/load-app.test.ts +0 -15
- package/src/__tests__/survey.test.ts +0 -159
- package/src/__tests__/warden.test.ts +0 -74
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create.adapter` trail -- Scaffold an adapter authoring target.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
adapterSourceExportKind,
|
|
7
|
+
adapterSourceExports,
|
|
8
|
+
adapterTargetPlacements,
|
|
9
|
+
checkAdapters,
|
|
10
|
+
deriveAdapterTargetCatalog,
|
|
11
|
+
} from '@ontrails/adapter-kit';
|
|
12
|
+
import type {
|
|
13
|
+
AdapterCheckDiagnostic,
|
|
14
|
+
AdapterTargetCatalogEntry,
|
|
15
|
+
AdapterTargetConformanceManifest,
|
|
16
|
+
AdapterTargetPlacement,
|
|
17
|
+
} from '@ontrails/adapter-kit';
|
|
18
|
+
import {
|
|
19
|
+
findWorkspacePackage,
|
|
20
|
+
listWorkspacePatterns,
|
|
21
|
+
Result,
|
|
22
|
+
trail,
|
|
23
|
+
ValidationError,
|
|
24
|
+
} from '@ontrails/core';
|
|
25
|
+
import type { WorkspaceRootManifest } from '@ontrails/core';
|
|
26
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
27
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
applyProjectOperations,
|
|
32
|
+
planProjectOperations,
|
|
33
|
+
resolveProjectPath,
|
|
34
|
+
} from '../project-writes.js';
|
|
35
|
+
import type {
|
|
36
|
+
PlannedProjectOperation,
|
|
37
|
+
ProjectWriteOperation,
|
|
38
|
+
} from '../project-writes.js';
|
|
39
|
+
import { trailsPackageVersion } from '../versions.js';
|
|
40
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
41
|
+
|
|
42
|
+
const adapterNamePattern = /^[a-z][a-z0-9-]*$/u;
|
|
43
|
+
const adapterNameMessage =
|
|
44
|
+
'Adapter name must be kebab-case, start with a lowercase letter, and contain only lowercase letters, digits, or "-".';
|
|
45
|
+
const packageNamePattern =
|
|
46
|
+
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
|
|
47
|
+
const packageNameMessage =
|
|
48
|
+
'Package name must be a valid lowercase npm package name, optionally scoped.';
|
|
49
|
+
|
|
50
|
+
type CreateAdapterPlacement = AdapterTargetPlacement;
|
|
51
|
+
|
|
52
|
+
const createAdapterPlacements = adapterTargetPlacements;
|
|
53
|
+
|
|
54
|
+
interface CreateAdapterInput {
|
|
55
|
+
readonly dryRun: boolean;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly packageName?: string | undefined;
|
|
58
|
+
readonly placement: CreateAdapterPlacement;
|
|
59
|
+
readonly rootDir?: string | undefined;
|
|
60
|
+
readonly target: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface CreateAdapterResult {
|
|
64
|
+
readonly adapterImport: string;
|
|
65
|
+
readonly created: readonly string[];
|
|
66
|
+
readonly diagnostics: readonly AdapterCheckDiagnostic[];
|
|
67
|
+
readonly dryRun: boolean;
|
|
68
|
+
readonly packageName: string;
|
|
69
|
+
readonly placement: CreateAdapterPlacement;
|
|
70
|
+
readonly plannedOperations: readonly PlannedProjectOperation[];
|
|
71
|
+
readonly targetKey: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface AdapterOperationPlan {
|
|
75
|
+
readonly adapterImport: string;
|
|
76
|
+
readonly packageName: string;
|
|
77
|
+
readonly operations: readonly ProjectWriteOperation[];
|
|
78
|
+
readonly targetKey: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface WorkspacePackageManifest {
|
|
82
|
+
readonly exports?: unknown;
|
|
83
|
+
readonly name?: unknown;
|
|
84
|
+
readonly trails?: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const literal = (value: string): string => JSON.stringify(value);
|
|
88
|
+
|
|
89
|
+
const writeOperation = (
|
|
90
|
+
path: string,
|
|
91
|
+
content: string
|
|
92
|
+
): ProjectWriteOperation => ({
|
|
93
|
+
content,
|
|
94
|
+
kind: 'write',
|
|
95
|
+
path,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const formatJson = (value: unknown): string =>
|
|
99
|
+
`${JSON.stringify(value, null, 2)}\n`;
|
|
100
|
+
|
|
101
|
+
const defaultExtractedPackageName = (name: string): string =>
|
|
102
|
+
`@ontrails/${name}`;
|
|
103
|
+
|
|
104
|
+
const fail = (message: string): Result<never, ValidationError> =>
|
|
105
|
+
Result.err(new ValidationError(message));
|
|
106
|
+
|
|
107
|
+
const readJson = <T>(path: string): T | undefined => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(readFileSync(path, 'utf8')) as T;
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const normalizeWorkspacePattern = (pattern: string): string =>
|
|
116
|
+
pattern.replace(/^\.\//u, '').replace(/\/+$/u, '');
|
|
117
|
+
|
|
118
|
+
const workspacePatternCoversPath = (
|
|
119
|
+
pattern: string,
|
|
120
|
+
workspacePath: string
|
|
121
|
+
): boolean => {
|
|
122
|
+
const normalized = normalizeWorkspacePattern(pattern);
|
|
123
|
+
if (normalized.endsWith('/*')) {
|
|
124
|
+
const prefix = normalized.slice(0, -2);
|
|
125
|
+
const rest = workspacePath.slice(prefix.length + 1);
|
|
126
|
+
return (
|
|
127
|
+
workspacePath.startsWith(`${prefix}/`) &&
|
|
128
|
+
rest.length > 0 &&
|
|
129
|
+
!rest.includes('/')
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return normalized === workspacePath;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const rootWorkspaceIncludesPath = (
|
|
137
|
+
rootDir: string,
|
|
138
|
+
workspacePath: string
|
|
139
|
+
): boolean => {
|
|
140
|
+
const rootManifest = readJson<WorkspaceRootManifest>(
|
|
141
|
+
join(rootDir, 'package.json')
|
|
142
|
+
);
|
|
143
|
+
return listWorkspacePatterns(rootManifest).some((pattern) =>
|
|
144
|
+
workspacePatternCoversPath(pattern, workspacePath)
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const resolvePhysicalRootDir = (
|
|
149
|
+
rootDir: string
|
|
150
|
+
): Result<string, ValidationError> => {
|
|
151
|
+
try {
|
|
152
|
+
return Result.ok(realpathSync(resolve(rootDir)));
|
|
153
|
+
} catch {
|
|
154
|
+
return fail(
|
|
155
|
+
`Workspace root "${rootDir}" does not exist or cannot be read.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
161
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
162
|
+
|
|
163
|
+
const toMutableRecord = (value: unknown): Record<string, unknown> =>
|
|
164
|
+
isRecord(value) ? { ...value } : {};
|
|
165
|
+
|
|
166
|
+
const runtimeExportTarget = (value: unknown, depth = 0): string | undefined => {
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
if (!isRecord(value) || depth > 8) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const condition of ['bun', 'import', 'default', 'require'] as const) {
|
|
175
|
+
const candidate = runtimeExportTarget(value[condition], depth + 1);
|
|
176
|
+
if (candidate) {
|
|
177
|
+
return candidate;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return undefined;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const packageRootExportTarget = (
|
|
185
|
+
target: AdapterTargetCatalogEntry
|
|
186
|
+
): string | undefined => {
|
|
187
|
+
const manifest = readJson<WorkspacePackageManifest>(target.packageJsonPath);
|
|
188
|
+
let exportTarget: string | undefined;
|
|
189
|
+
if (typeof manifest?.exports === 'string') {
|
|
190
|
+
exportTarget = manifest.exports;
|
|
191
|
+
} else if (isRecord(manifest?.exports)) {
|
|
192
|
+
const rootExport = Object.hasOwn(manifest.exports, '.')
|
|
193
|
+
? manifest.exports['.']
|
|
194
|
+
: manifest.exports;
|
|
195
|
+
exportTarget = runtimeExportTarget(rootExport);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!exportTarget || exportTarget.startsWith('..')) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return resolve(target.packageRoot, exportTarget);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const relativeImportSpecifier = (fromFile: string, toFile: string): string => {
|
|
206
|
+
const withoutExtension = relative(dirname(fromFile), toFile)
|
|
207
|
+
.replaceAll('\\', '/')
|
|
208
|
+
.replace(/\.ts$/u, '.js');
|
|
209
|
+
return withoutExtension.startsWith('.')
|
|
210
|
+
? withoutExtension
|
|
211
|
+
: `./${withoutExtension}`;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const assertHttpOwnerSupport = (
|
|
215
|
+
target: AdapterTargetCatalogEntry
|
|
216
|
+
): Result<void, ValidationError> => {
|
|
217
|
+
const rootExportTarget = packageRootExportTarget(target);
|
|
218
|
+
if (!rootExportTarget || !existsSync(rootExportTarget)) {
|
|
219
|
+
return fail(
|
|
220
|
+
`Adapter target "${target.key}" cannot use the HTTP create.adapter template because ${target.ownerPackage} does not expose a readable package root export.`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const requiredExports = [
|
|
225
|
+
['createFetchHandler', 'value', false],
|
|
226
|
+
['CreateFetchHandlerOptions', 'type', true],
|
|
227
|
+
] as const;
|
|
228
|
+
const missing = requiredExports.filter(([identifier, expected, typeOnly]) => {
|
|
229
|
+
if (typeOnly) {
|
|
230
|
+
const exportKind = adapterSourceExportKind(rootExportTarget, identifier);
|
|
231
|
+
return (
|
|
232
|
+
exportKind !== 'type' &&
|
|
233
|
+
exportKind !== 'interface-value' &&
|
|
234
|
+
exportKind !== 'interface-value-erased' &&
|
|
235
|
+
exportKind !== 'type-alias-value' &&
|
|
236
|
+
exportKind !== 'type-alias-value-erased'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return !adapterSourceExports(rootExportTarget, identifier, expected);
|
|
240
|
+
});
|
|
241
|
+
if (missing.length === 0) {
|
|
242
|
+
return Result.ok();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return fail(
|
|
246
|
+
`Adapter target "${target.key}" cannot use the HTTP create.adapter template because ${target.ownerPackage} does not export ${missing
|
|
247
|
+
.map(([identifier]) => identifier)
|
|
248
|
+
.join(' and ')} from its package root.`
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const assertHttpTemplate = (
|
|
253
|
+
target: AdapterTargetCatalogEntry
|
|
254
|
+
): Result<void, ValidationError> => {
|
|
255
|
+
if (target.target !== 'http') {
|
|
256
|
+
return fail(
|
|
257
|
+
`Adapter target "${target.target}" does not yet expose a create.adapter starter template.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return assertHttpOwnerSupport(target);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const assertConformanceScaffold = (
|
|
265
|
+
target: AdapterTargetCatalogEntry
|
|
266
|
+
): Result<void, ValidationError> => {
|
|
267
|
+
if (target.testingImport && target.conformance) {
|
|
268
|
+
return Result.ok();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return fail(
|
|
272
|
+
`Adapter target "${target.key}" does not declare testingImport and conformance metadata for scaffolded conformance.`
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const resolveTarget = (
|
|
277
|
+
rootDir: string,
|
|
278
|
+
target: string,
|
|
279
|
+
placement: CreateAdapterPlacement
|
|
280
|
+
): Result<AdapterTargetCatalogEntry, ValidationError> => {
|
|
281
|
+
const catalog = deriveAdapterTargetCatalog(rootDir);
|
|
282
|
+
if (catalog.diagnostics.length > 0) {
|
|
283
|
+
return fail(
|
|
284
|
+
[
|
|
285
|
+
'Adapter target catalog has diagnostics; fix them before scaffolding:',
|
|
286
|
+
...catalog.diagnostics.map((entry) => `- ${entry.message}`),
|
|
287
|
+
].join('\n')
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const entry = catalog.targets.find(
|
|
292
|
+
(candidate) => candidate.target === target
|
|
293
|
+
);
|
|
294
|
+
if (entry === undefined) {
|
|
295
|
+
return fail(`Unknown adapter target "${target}".`);
|
|
296
|
+
}
|
|
297
|
+
if (!entry.placements.includes(placement)) {
|
|
298
|
+
return fail(
|
|
299
|
+
`Adapter target "${entry.key}" does not support ${placement} placement.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const conformance = assertConformanceScaffold(entry);
|
|
304
|
+
if (conformance.isErr()) {
|
|
305
|
+
return conformance;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const template = assertHttpTemplate(entry);
|
|
309
|
+
if (template.isErr()) {
|
|
310
|
+
return template;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Result.ok(entry);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const generateExtractedPackageJson = (
|
|
317
|
+
packageName: string,
|
|
318
|
+
target: AdapterTargetCatalogEntry
|
|
319
|
+
): string =>
|
|
320
|
+
formatJson({
|
|
321
|
+
dependencies: {
|
|
322
|
+
'@ontrails/core': 'workspace:^',
|
|
323
|
+
},
|
|
324
|
+
exports: {
|
|
325
|
+
'.': './src/index.ts',
|
|
326
|
+
'./package.json': './package.json',
|
|
327
|
+
},
|
|
328
|
+
files: [
|
|
329
|
+
'src/**/*.ts',
|
|
330
|
+
'!src/**/__tests__/**',
|
|
331
|
+
'!src/**/*.test.ts',
|
|
332
|
+
'!src/**/*.test-d.ts',
|
|
333
|
+
'README.md',
|
|
334
|
+
],
|
|
335
|
+
name: packageName,
|
|
336
|
+
peerDependencies: {
|
|
337
|
+
[target.ownerPackage]: 'workspace:^',
|
|
338
|
+
},
|
|
339
|
+
scripts: {
|
|
340
|
+
build: 'tsc -b',
|
|
341
|
+
clean: 'rm -rf dist *.tsbuildinfo',
|
|
342
|
+
lint: 'oxlint ./src',
|
|
343
|
+
test: 'bun test',
|
|
344
|
+
typecheck: 'tsc --noEmit',
|
|
345
|
+
},
|
|
346
|
+
trails: {
|
|
347
|
+
adapter: {
|
|
348
|
+
target: target.target,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
type: 'module',
|
|
352
|
+
version: trailsPackageVersion,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const generateExtractedTsconfig = (): string =>
|
|
356
|
+
formatJson({
|
|
357
|
+
compilerOptions: {
|
|
358
|
+
outDir: 'dist',
|
|
359
|
+
rootDir: 'src',
|
|
360
|
+
},
|
|
361
|
+
exclude: ['**/__tests__/**', '**/*.test.ts', 'dist'],
|
|
362
|
+
extends: '../../tsconfig.json',
|
|
363
|
+
include: ['src'],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const generateTestTsconfig = (): string =>
|
|
367
|
+
formatJson({
|
|
368
|
+
compilerOptions: {
|
|
369
|
+
noEmit: true,
|
|
370
|
+
rootDir: './src',
|
|
371
|
+
types: ['bun'],
|
|
372
|
+
},
|
|
373
|
+
exclude: [],
|
|
374
|
+
extends: './tsconfig.json',
|
|
375
|
+
include: ['src/**/*.test.ts', 'src/__tests__/**/*.ts'],
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const generateHttpExtractedIndex = (
|
|
379
|
+
target: AdapterTargetCatalogEntry
|
|
380
|
+
): string =>
|
|
381
|
+
`import type { Topo } from '@ontrails/core';
|
|
382
|
+
import { createFetchHandler } from ${literal(target.ownerPackage)};
|
|
383
|
+
import type { CreateFetchHandlerOptions } from ${literal(target.ownerPackage)};
|
|
384
|
+
|
|
385
|
+
export interface CreateAppOptions extends CreateFetchHandlerOptions {}
|
|
386
|
+
|
|
387
|
+
export const createApp = (
|
|
388
|
+
graph: Topo,
|
|
389
|
+
options: CreateAppOptions = {}
|
|
390
|
+
) => ({
|
|
391
|
+
fetch: createFetchHandler(graph, options),
|
|
392
|
+
});
|
|
393
|
+
`;
|
|
394
|
+
|
|
395
|
+
const generateHttpSubpathIndex = (supportImportPath: string): string =>
|
|
396
|
+
`import type { Topo } from '@ontrails/core';
|
|
397
|
+
import { createFetchHandler } from ${literal(supportImportPath)};
|
|
398
|
+
import type { CreateFetchHandlerOptions } from ${literal(supportImportPath)};
|
|
399
|
+
|
|
400
|
+
export interface CreateAppOptions extends CreateFetchHandlerOptions {}
|
|
401
|
+
|
|
402
|
+
export const createApp = (
|
|
403
|
+
graph: Topo,
|
|
404
|
+
options: CreateAppOptions = {}
|
|
405
|
+
) => ({
|
|
406
|
+
fetch: createFetchHandler(graph, options),
|
|
407
|
+
});
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
const generateConformanceTest = (
|
|
411
|
+
target: AdapterTargetCatalogEntry,
|
|
412
|
+
adapterImport: string,
|
|
413
|
+
createAppImportPath: string
|
|
414
|
+
): string => {
|
|
415
|
+
const conformance = target.conformance as AdapterTargetConformanceManifest;
|
|
416
|
+
return `import {
|
|
417
|
+
${conformance.casesFactory},
|
|
418
|
+
${conformance.runner},
|
|
419
|
+
} from ${literal(target.testingImport ?? '')};
|
|
420
|
+
import type { ${conformance.adapterType} } from ${literal(target.testingImport ?? '')};
|
|
421
|
+
|
|
422
|
+
import { createApp } from ${literal(createAppImportPath)};
|
|
423
|
+
|
|
424
|
+
const adapter = {
|
|
425
|
+
createApp,
|
|
426
|
+
name: ${literal(adapterImport)},
|
|
427
|
+
} satisfies ${conformance.adapterType};
|
|
428
|
+
|
|
429
|
+
await ${conformance.runner}(adapter, await ${conformance.casesFactory}());
|
|
430
|
+
`;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const generateReadme = (
|
|
434
|
+
packageName: string,
|
|
435
|
+
target: AdapterTargetCatalogEntry
|
|
436
|
+
): string =>
|
|
437
|
+
`# ${packageName}
|
|
438
|
+
|
|
439
|
+
${packageName} is a Trails ${target.target} adapter scaffold.
|
|
440
|
+
|
|
441
|
+
## Validate
|
|
442
|
+
|
|
443
|
+
\`\`\`bash
|
|
444
|
+
bun test
|
|
445
|
+
bun run typecheck
|
|
446
|
+
bun run lint
|
|
447
|
+
trails adapter check --root-dir ../..
|
|
448
|
+
\`\`\`
|
|
449
|
+
|
|
450
|
+
The conformance test imports ${target.testingImport} so owner-authored cases stay current as ${target.ownerPackage} evolves.
|
|
451
|
+
`;
|
|
452
|
+
|
|
453
|
+
const buildExtractedPlan = (
|
|
454
|
+
rootDir: string,
|
|
455
|
+
input: CreateAdapterInput,
|
|
456
|
+
target: AdapterTargetCatalogEntry
|
|
457
|
+
): Result<AdapterOperationPlan, Error> => {
|
|
458
|
+
const packageName =
|
|
459
|
+
input.packageName ?? defaultExtractedPackageName(input.name);
|
|
460
|
+
if (!packageNamePattern.test(packageName)) {
|
|
461
|
+
return fail(packageNameMessage);
|
|
462
|
+
}
|
|
463
|
+
const existingPackage = findWorkspacePackage<WorkspacePackageManifest>(
|
|
464
|
+
rootDir,
|
|
465
|
+
packageName
|
|
466
|
+
);
|
|
467
|
+
if (existingPackage) {
|
|
468
|
+
return fail(
|
|
469
|
+
`Workspace package name "${packageName}" already exists at ${existingPackage.workspacePath}.`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const packageRootPath = `adapters/${input.name}`;
|
|
474
|
+
if (!rootWorkspaceIncludesPath(rootDir, packageRootPath)) {
|
|
475
|
+
return fail(
|
|
476
|
+
`Root package.json workspaces must include "${packageRootPath}" or "adapters/*" before create.adapter can write an extracted adapter package.`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const packageRoot = resolveProjectPath(rootDir, packageRootPath);
|
|
481
|
+
if (packageRoot.isErr()) {
|
|
482
|
+
return packageRoot;
|
|
483
|
+
}
|
|
484
|
+
if (existsSync(packageRoot.value)) {
|
|
485
|
+
return fail(`Adapter package already exists at ${packageRootPath}.`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const adapterImport = packageName;
|
|
489
|
+
const operations = [
|
|
490
|
+
writeOperation(
|
|
491
|
+
`${packageRootPath}/package.json`,
|
|
492
|
+
generateExtractedPackageJson(packageName, target)
|
|
493
|
+
),
|
|
494
|
+
writeOperation(
|
|
495
|
+
`${packageRootPath}/tsconfig.json`,
|
|
496
|
+
generateExtractedTsconfig()
|
|
497
|
+
),
|
|
498
|
+
writeOperation(
|
|
499
|
+
`${packageRootPath}/tsconfig.tests.json`,
|
|
500
|
+
generateTestTsconfig()
|
|
501
|
+
),
|
|
502
|
+
writeOperation(
|
|
503
|
+
`${packageRootPath}/README.md`,
|
|
504
|
+
generateReadme(packageName, target)
|
|
505
|
+
),
|
|
506
|
+
writeOperation(
|
|
507
|
+
`${packageRootPath}/src/index.ts`,
|
|
508
|
+
generateHttpExtractedIndex(target)
|
|
509
|
+
),
|
|
510
|
+
writeOperation(
|
|
511
|
+
`${packageRootPath}/src/__tests__/conformance.test.ts`,
|
|
512
|
+
generateConformanceTest(target, adapterImport, '../index.js')
|
|
513
|
+
),
|
|
514
|
+
];
|
|
515
|
+
|
|
516
|
+
return Result.ok({
|
|
517
|
+
adapterImport,
|
|
518
|
+
operations,
|
|
519
|
+
packageName,
|
|
520
|
+
targetKey: target.key,
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const buildSubpathPlan = (
|
|
525
|
+
rootDir: string,
|
|
526
|
+
input: CreateAdapterInput,
|
|
527
|
+
target: AdapterTargetCatalogEntry
|
|
528
|
+
): Result<AdapterOperationPlan, Error> => {
|
|
529
|
+
if (input.packageName !== undefined) {
|
|
530
|
+
return fail(
|
|
531
|
+
'packageName is only supported for extracted adapter placement.'
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const ownerPackage = findWorkspacePackage<WorkspacePackageManifest>(
|
|
536
|
+
rootDir,
|
|
537
|
+
target.ownerPackage
|
|
538
|
+
);
|
|
539
|
+
if (!ownerPackage) {
|
|
540
|
+
return fail(
|
|
541
|
+
`Adapter target "${target.key}" owner package ${target.ownerPackage} is not a workspace package.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const manifest = readJson<WorkspacePackageManifest>(
|
|
546
|
+
ownerPackage.packageJsonPath
|
|
547
|
+
);
|
|
548
|
+
if (!manifest || !isRecord(manifest)) {
|
|
549
|
+
return fail(
|
|
550
|
+
`Adapter target "${target.key}" owner package manifest could not be read.`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const manifestExports = manifest['exports'];
|
|
555
|
+
if (
|
|
556
|
+
manifestExports !== undefined &&
|
|
557
|
+
typeof manifestExports !== 'string' &&
|
|
558
|
+
!isRecord(manifestExports)
|
|
559
|
+
) {
|
|
560
|
+
return fail(
|
|
561
|
+
`${target.ownerPackage} package.json exports must be an object before create.adapter can add a subpath adapter.`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const exportKey = `./${input.name}`;
|
|
566
|
+
const packageExports =
|
|
567
|
+
typeof manifestExports === 'string'
|
|
568
|
+
? { '.': manifestExports }
|
|
569
|
+
: toMutableRecord(manifestExports);
|
|
570
|
+
if (Object.hasOwn(packageExports, exportKey)) {
|
|
571
|
+
return fail(`${target.ownerPackage} already exports "${exportKey}".`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const manifestTrails = manifest['trails'];
|
|
575
|
+
const trails = toMutableRecord(manifestTrails);
|
|
576
|
+
if (manifestTrails !== undefined && !isRecord(manifestTrails)) {
|
|
577
|
+
return fail(
|
|
578
|
+
`${target.ownerPackage} package.json trails must be an object.`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const adapters = toMutableRecord(trails['adapters']);
|
|
583
|
+
if (trails['adapters'] !== undefined && !isRecord(trails['adapters'])) {
|
|
584
|
+
return fail(
|
|
585
|
+
`${target.ownerPackage} package.json trails.adapters must be an object before create.adapter can add a subpath adapter.`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (Object.hasOwn(adapters, exportKey)) {
|
|
589
|
+
return fail(
|
|
590
|
+
`${target.ownerPackage} package.json trails.adapters already declares "${exportKey}".`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const sourcePath = `${ownerPackage.workspacePath}/src/${input.name}/index.ts`;
|
|
595
|
+
const sourceRoot = resolveProjectPath(
|
|
596
|
+
rootDir,
|
|
597
|
+
`${ownerPackage.workspacePath}/src/${input.name}`
|
|
598
|
+
);
|
|
599
|
+
if (sourceRoot.isErr()) {
|
|
600
|
+
return sourceRoot;
|
|
601
|
+
}
|
|
602
|
+
if (existsSync(sourceRoot.value)) {
|
|
603
|
+
return fail(
|
|
604
|
+
`Subpath adapter source already exists at ${ownerPackage.workspacePath}/src/${input.name}.`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const supportTarget = packageRootExportTarget(target);
|
|
609
|
+
if (!supportTarget) {
|
|
610
|
+
return fail(
|
|
611
|
+
`Adapter target "${target.key}" cannot use the HTTP subpath template because ${target.ownerPackage} does not expose a readable package root export.`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
packageExports[exportKey] = `./src/${input.name}/index.ts`;
|
|
616
|
+
adapters[exportKey] = { target: target.target };
|
|
617
|
+
trails['adapters'] = adapters;
|
|
618
|
+
|
|
619
|
+
const adapterImport = `${target.ownerPackage}/${input.name}`;
|
|
620
|
+
const sourceFile = resolve(rootDir, sourcePath);
|
|
621
|
+
const packageJson = formatJson({
|
|
622
|
+
...manifest,
|
|
623
|
+
exports: packageExports,
|
|
624
|
+
trails,
|
|
625
|
+
});
|
|
626
|
+
const operations = [
|
|
627
|
+
writeOperation(`${ownerPackage.workspacePath}/package.json`, packageJson),
|
|
628
|
+
writeOperation(
|
|
629
|
+
sourcePath,
|
|
630
|
+
generateHttpSubpathIndex(
|
|
631
|
+
relativeImportSpecifier(sourceFile, supportTarget)
|
|
632
|
+
)
|
|
633
|
+
),
|
|
634
|
+
writeOperation(
|
|
635
|
+
`${ownerPackage.workspacePath}/src/${input.name}/__tests__/conformance.test.ts`,
|
|
636
|
+
generateConformanceTest(target, adapterImport, '../index.js')
|
|
637
|
+
),
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
return Result.ok({
|
|
641
|
+
adapterImport,
|
|
642
|
+
operations,
|
|
643
|
+
packageName: adapterImport,
|
|
644
|
+
targetKey: target.key,
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const buildOperationPlan = (
|
|
649
|
+
rootDir: string,
|
|
650
|
+
input: CreateAdapterInput,
|
|
651
|
+
target: AdapterTargetCatalogEntry
|
|
652
|
+
): Result<AdapterOperationPlan, Error> =>
|
|
653
|
+
input.placement === 'extracted'
|
|
654
|
+
? buildExtractedPlan(rootDir, input, target)
|
|
655
|
+
: buildSubpathPlan(rootDir, input, target);
|
|
656
|
+
|
|
657
|
+
const runPlannedOperations = async (
|
|
658
|
+
rootDir: string,
|
|
659
|
+
operations: readonly ProjectWriteOperation[],
|
|
660
|
+
dryRun: boolean
|
|
661
|
+
): Promise<Result<readonly PlannedProjectOperation[], Error>> =>
|
|
662
|
+
dryRun
|
|
663
|
+
? planProjectOperations(rootDir, operations)
|
|
664
|
+
: await applyProjectOperations(rootDir, operations);
|
|
665
|
+
|
|
666
|
+
export const createAdapterTrail = trail('create.adapter', {
|
|
667
|
+
args: ['name'],
|
|
668
|
+
description: 'Scaffold an adapter package from adapter target catalog facts',
|
|
669
|
+
fields: {
|
|
670
|
+
placement: {
|
|
671
|
+
options: [
|
|
672
|
+
{
|
|
673
|
+
hint: 'Standalone package under adapters/',
|
|
674
|
+
label: 'Extracted',
|
|
675
|
+
value: 'extracted',
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
hint: 'Owner package subpath export',
|
|
679
|
+
label: 'Subpath',
|
|
680
|
+
value: 'subpath',
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
implementation: async (input: CreateAdapterInput, ctx) => {
|
|
686
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
687
|
+
if (rootDirResult.isErr()) {
|
|
688
|
+
return rootDirResult;
|
|
689
|
+
}
|
|
690
|
+
const physicalRootDir = resolvePhysicalRootDir(rootDirResult.value);
|
|
691
|
+
if (physicalRootDir.isErr()) {
|
|
692
|
+
return physicalRootDir;
|
|
693
|
+
}
|
|
694
|
+
const rootDir = physicalRootDir.value;
|
|
695
|
+
const target = resolveTarget(rootDir, input.target, input.placement);
|
|
696
|
+
if (target.isErr()) {
|
|
697
|
+
return target;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const plan = buildOperationPlan(rootDir, input, target.value);
|
|
701
|
+
if (plan.isErr()) {
|
|
702
|
+
return plan;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const plannedOperations = await runPlannedOperations(
|
|
706
|
+
rootDir,
|
|
707
|
+
plan.value.operations,
|
|
708
|
+
input.dryRun
|
|
709
|
+
);
|
|
710
|
+
if (plannedOperations.isErr()) {
|
|
711
|
+
return plannedOperations;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const report = checkAdapters(rootDir);
|
|
715
|
+
const created = input.dryRun
|
|
716
|
+
? []
|
|
717
|
+
: plannedOperations.value
|
|
718
|
+
.filter((operation) => operation.kind === 'write')
|
|
719
|
+
.map((operation) => operation.path);
|
|
720
|
+
|
|
721
|
+
return Result.ok({
|
|
722
|
+
adapterImport: plan.value.adapterImport,
|
|
723
|
+
created,
|
|
724
|
+
diagnostics: [...report.diagnostics],
|
|
725
|
+
dryRun: input.dryRun,
|
|
726
|
+
packageName: plan.value.packageName,
|
|
727
|
+
placement: input.placement,
|
|
728
|
+
plannedOperations: [...plannedOperations.value],
|
|
729
|
+
targetKey: plan.value.targetKey,
|
|
730
|
+
} satisfies CreateAdapterResult);
|
|
731
|
+
},
|
|
732
|
+
input: z.object({
|
|
733
|
+
dryRun: z
|
|
734
|
+
.boolean()
|
|
735
|
+
.default(false)
|
|
736
|
+
.describe('Plan adapter scaffold writes without touching disk'),
|
|
737
|
+
name: z
|
|
738
|
+
.string()
|
|
739
|
+
.regex(adapterNamePattern, adapterNameMessage)
|
|
740
|
+
.describe('Adapter name, e.g. hono'),
|
|
741
|
+
packageName: z
|
|
742
|
+
.string()
|
|
743
|
+
.regex(packageNamePattern, packageNameMessage)
|
|
744
|
+
.optional()
|
|
745
|
+
.describe('Package name for extracted adapter placement'),
|
|
746
|
+
placement: z
|
|
747
|
+
.enum(createAdapterPlacements)
|
|
748
|
+
.default('extracted')
|
|
749
|
+
.describe('Adapter placement'),
|
|
750
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
751
|
+
target: z.string().describe('Adapter target id, e.g. http'),
|
|
752
|
+
}),
|
|
753
|
+
intent: 'write',
|
|
754
|
+
output: z.object({
|
|
755
|
+
adapterImport: z.string(),
|
|
756
|
+
created: z.array(z.string()).readonly(),
|
|
757
|
+
diagnostics: z.array(
|
|
758
|
+
z.object({
|
|
759
|
+
code: z.string(),
|
|
760
|
+
message: z.string(),
|
|
761
|
+
packageJsonPath: z.string(),
|
|
762
|
+
packageName: z.string().optional(),
|
|
763
|
+
placement: z.enum(adapterTargetPlacements).optional(),
|
|
764
|
+
severity: z.enum(['error', 'warn']),
|
|
765
|
+
target: z.string().optional(),
|
|
766
|
+
})
|
|
767
|
+
),
|
|
768
|
+
dryRun: z.boolean(),
|
|
769
|
+
packageName: z.string(),
|
|
770
|
+
placement: z.enum(createAdapterPlacements),
|
|
771
|
+
plannedOperations: z.array(
|
|
772
|
+
z.discriminatedUnion('kind', [
|
|
773
|
+
z.object({ kind: z.literal('mkdir'), path: z.string() }),
|
|
774
|
+
z.object({
|
|
775
|
+
from: z.string(),
|
|
776
|
+
kind: z.literal('rename'),
|
|
777
|
+
to: z.string(),
|
|
778
|
+
}),
|
|
779
|
+
z.object({ kind: z.literal('write'), path: z.string() }),
|
|
780
|
+
])
|
|
781
|
+
),
|
|
782
|
+
targetKey: z.string(),
|
|
783
|
+
}),
|
|
784
|
+
permit: { scopes: ['project:write'] },
|
|
785
|
+
});
|