@ontrails/trails 1.0.0-beta.2 → 1.0.0-beta.22
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 +647 -0
- package/README.md +26 -0
- package/package.json +28 -7
- package/src/app.ts +86 -2
- package/src/clack.ts +22 -0
- package/src/cli.ts +330 -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 +153 -0
- package/src/mcp-app.ts +30 -0
- package/src/mcp-options.ts +77 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/release/bindings.ts +39 -0
- package/src/release/check.ts +818 -0
- package/src/release/config.ts +63 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/index.ts +85 -0
- package/src/release/native-bun-publish.ts +651 -0
- package/src/release/native-bun-registry.ts +350 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/smoke.ts +46 -0
- package/src/release/wayfinder-dogfood-smoke.ts +226 -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-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -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 +94 -40
- package/src/trails/add-trail.ts +79 -41
- package/src/trails/add-verify.ts +95 -25
- package/src/trails/compile.ts +67 -0
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-adapter.ts +1084 -0
- package/src/trails/create-scaffold.ts +399 -104
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +185 -71
- 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 +340 -0
- package/src/trails/doctor.ts +56 -0
- package/src/trails/draft-promote.ts +949 -0
- package/src/trails/guide.ts +74 -68
- package/src/trails/load-app.ts +1143 -15
- package/src/trails/project.ts +17 -3
- package/src/trails/release-check.ts +104 -0
- package/src/trails/release-smoke.ts +48 -0
- package/src/trails/revise.ts +53 -0
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +491 -0
- package/src/trails/run-examples.ts +145 -0
- package/src/trails/run.ts +410 -0
- package/src/trails/scaffold-json.ts +58 -0
- package/src/trails/survey.ts +881 -226
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-history.ts +47 -0
- package/src/trails/topo-output-schemas.ts +248 -0
- package/src/trails/topo-pin.ts +52 -0
- package/src/trails/topo-read-support.ts +313 -0
- package/src/trails/topo-reports.ts +807 -0
- package/src/trails/topo-store-support.ts +174 -0
- package/src/trails/topo-support.ts +220 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +106 -0
- package/src/trails/validate.ts +38 -0
- package/src/trails/version-lifecycle-support.ts +945 -0
- package/src/trails/warden-guide.ts +129 -0
- package/src/trails/warden.ts +165 -58
- 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 -62
- 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 -11
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -85
- 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 -33
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -225
- 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 -161
- package/src/__tests__/warden.test.ts +0 -74
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `adapter.check` trail -- Local adapter authoring readiness checks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { adapterTargetPlacements, checkAdapters } from '@ontrails/adapter-kit';
|
|
6
|
+
import type { AdapterCheckReport } from '@ontrails/adapter-kit';
|
|
7
|
+
import { isPlainObject, Result, trail, ValidationError } from '@ontrails/core';
|
|
8
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
9
|
+
import { join, relative } from 'node:path';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
13
|
+
|
|
14
|
+
const adapterCheckInputSchema = z.object({
|
|
15
|
+
rootDir: z.string().optional().describe('Root directory to scan'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const adapterPlacementSchema = z.enum(adapterTargetPlacements);
|
|
19
|
+
|
|
20
|
+
const adapterCheckDiagnosticSchema = z.object({
|
|
21
|
+
code: z.string().describe('Stable adapter diagnostic code'),
|
|
22
|
+
message: z.string(),
|
|
23
|
+
packageJsonPath: z.string(),
|
|
24
|
+
packageName: z.string().optional(),
|
|
25
|
+
placement: adapterPlacementSchema.optional(),
|
|
26
|
+
severity: z.enum(['error', 'warn']),
|
|
27
|
+
target: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const adapterCheckSubjectSchema = z.object({
|
|
31
|
+
conformanceTestPaths: z.array(z.string()).readonly(),
|
|
32
|
+
key: z.string(),
|
|
33
|
+
ownerPackage: z.string(),
|
|
34
|
+
packageJsonPath: z.string(),
|
|
35
|
+
packageName: z.string(),
|
|
36
|
+
packageRoot: z.string(),
|
|
37
|
+
placement: adapterPlacementSchema,
|
|
38
|
+
target: z.string(),
|
|
39
|
+
targetKey: z.string(),
|
|
40
|
+
testingImport: z.string().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const adapterTargetSchema = z.object({
|
|
44
|
+
conformance: z
|
|
45
|
+
.object({
|
|
46
|
+
adapterType: z.string(),
|
|
47
|
+
casesFactory: z.string(),
|
|
48
|
+
runner: z.string(),
|
|
49
|
+
})
|
|
50
|
+
.optional(),
|
|
51
|
+
key: z.string(),
|
|
52
|
+
ownerPackage: z.string(),
|
|
53
|
+
packageJsonPath: z.string(),
|
|
54
|
+
packageRoot: z.string(),
|
|
55
|
+
placements: z.array(adapterPlacementSchema).readonly(),
|
|
56
|
+
supportExportTarget: z.string().optional(),
|
|
57
|
+
supportImport: z.string().optional(),
|
|
58
|
+
target: z.string(),
|
|
59
|
+
testingExportTarget: z.string().optional(),
|
|
60
|
+
testingImport: z.string().optional(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const adapterCheckOutputSchema = z.object({
|
|
64
|
+
diagnostics: z.array(adapterCheckDiagnosticSchema).readonly(),
|
|
65
|
+
formatted: z.string(),
|
|
66
|
+
passed: z.boolean(),
|
|
67
|
+
subjects: z.array(adapterCheckSubjectSchema).readonly(),
|
|
68
|
+
targets: z.array(adapterTargetSchema).readonly(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const relativeToRoot = (rootDir: string, path: string): string => {
|
|
72
|
+
const normalized = relative(rootDir, path).replaceAll('\\', '/');
|
|
73
|
+
return normalized.length === 0 || normalized.startsWith('..')
|
|
74
|
+
? path
|
|
75
|
+
: normalized;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const workspacePatternsFromManifest = (
|
|
79
|
+
manifest: Readonly<Record<string, unknown>>
|
|
80
|
+
): readonly string[] => {
|
|
81
|
+
const { workspaces } = manifest;
|
|
82
|
+
if (Array.isArray(workspaces)) {
|
|
83
|
+
return workspaces.filter(
|
|
84
|
+
(pattern): pattern is string => typeof pattern === 'string'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const packages = isPlainObject(workspaces)
|
|
89
|
+
? workspaces['packages']
|
|
90
|
+
: undefined;
|
|
91
|
+
return Array.isArray(packages)
|
|
92
|
+
? packages.filter(
|
|
93
|
+
(pattern): pattern is string => typeof pattern === 'string'
|
|
94
|
+
)
|
|
95
|
+
: [];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const readWorkspaceManifest = (
|
|
99
|
+
packageJsonPath: string
|
|
100
|
+
): Result<Readonly<Record<string, unknown>>, ValidationError> => {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
103
|
+
return isPlainObject(parsed)
|
|
104
|
+
? Result.ok(parsed)
|
|
105
|
+
: Result.err(
|
|
106
|
+
new ValidationError(
|
|
107
|
+
`adapter.check root package.json must contain a JSON object: "${packageJsonPath}"`
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return Result.err(
|
|
112
|
+
new ValidationError(
|
|
113
|
+
`adapter.check could not read root package.json: "${packageJsonPath}"`,
|
|
114
|
+
error instanceof Error ? { cause: error } : undefined
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const validateAdapterCheckRoot = (
|
|
121
|
+
rootDir: string
|
|
122
|
+
): Result<void, ValidationError> => {
|
|
123
|
+
if (!existsSync(rootDir)) {
|
|
124
|
+
return Result.err(
|
|
125
|
+
new ValidationError(`adapter.check rootDir does not exist: "${rootDir}"`)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!statSync(rootDir).isDirectory()) {
|
|
130
|
+
return Result.err(
|
|
131
|
+
new ValidationError(
|
|
132
|
+
`adapter.check rootDir must be a directory: "${rootDir}"`
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const packageJsonPath = join(rootDir, 'package.json');
|
|
138
|
+
if (!existsSync(packageJsonPath)) {
|
|
139
|
+
return Result.err(
|
|
140
|
+
new ValidationError(
|
|
141
|
+
`adapter.check rootDir must contain a package.json workspace manifest: "${packageJsonPath}"`
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manifest = readWorkspaceManifest(packageJsonPath);
|
|
147
|
+
if (manifest.isErr()) {
|
|
148
|
+
return Result.err(manifest.error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (workspacePatternsFromManifest(manifest.value).length === 0) {
|
|
152
|
+
return Result.err(
|
|
153
|
+
new ValidationError(
|
|
154
|
+
`adapter.check root package.json must declare workspace packages: "${packageJsonPath}"`
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Result.ok();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const formatAdapterCheckReport = (
|
|
163
|
+
report: AdapterCheckReport,
|
|
164
|
+
rootDir: string
|
|
165
|
+
): string => {
|
|
166
|
+
const passed = report.diagnostics.length === 0;
|
|
167
|
+
const lines = [
|
|
168
|
+
'## Adapter Check Report',
|
|
169
|
+
'',
|
|
170
|
+
`Result: ${passed ? 'PASS' : 'FAIL'}`,
|
|
171
|
+
`Targets: ${report.targets.length}`,
|
|
172
|
+
`Adapters: ${report.subjects.length}`,
|
|
173
|
+
`Diagnostics: ${report.diagnostics.length}`,
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
if (report.targets.length > 0) {
|
|
177
|
+
lines.push('', '### Targets');
|
|
178
|
+
for (const target of report.targets) {
|
|
179
|
+
lines.push(
|
|
180
|
+
`- ${target.key} (${target.placements.join(', ')}) from ${relativeToRoot(rootDir, target.packageJsonPath)}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (report.subjects.length > 0) {
|
|
186
|
+
lines.push('', '### Adapters');
|
|
187
|
+
for (const subject of report.subjects) {
|
|
188
|
+
const conformance =
|
|
189
|
+
subject.conformanceTestPaths.length === 0
|
|
190
|
+
? 'no conformance tests'
|
|
191
|
+
: `${subject.conformanceTestPaths.length} conformance test(s)`;
|
|
192
|
+
lines.push(
|
|
193
|
+
`- ${subject.packageName} -> ${subject.targetKey} (${subject.placement}, ${conformance})`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (report.diagnostics.length > 0) {
|
|
199
|
+
lines.push('', '### Diagnostics');
|
|
200
|
+
for (const diagnostic of report.diagnostics) {
|
|
201
|
+
lines.push(
|
|
202
|
+
`- ${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${relativeToRoot(rootDir, diagnostic.packageJsonPath)}: ${diagnostic.message}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const adapterCheckTrail = trail('adapter.check', {
|
|
211
|
+
blaze: (input, ctx) => {
|
|
212
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
213
|
+
if (rootDirResult.isErr()) {
|
|
214
|
+
return rootDirResult;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const rootDir = rootDirResult.value;
|
|
218
|
+
const validRoot = validateAdapterCheckRoot(rootDir);
|
|
219
|
+
if (validRoot.isErr()) {
|
|
220
|
+
return validRoot;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const report = checkAdapters(rootDir);
|
|
224
|
+
|
|
225
|
+
return Result.ok({
|
|
226
|
+
diagnostics: [...report.diagnostics],
|
|
227
|
+
formatted: formatAdapterCheckReport(report, rootDir),
|
|
228
|
+
passed: report.diagnostics.length === 0,
|
|
229
|
+
subjects: [...report.subjects],
|
|
230
|
+
targets: [...report.targets],
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
description: 'Check adapter authoring readiness',
|
|
234
|
+
examples: [
|
|
235
|
+
{
|
|
236
|
+
input: {},
|
|
237
|
+
name: 'Check adapters in the current workspace',
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
input: adapterCheckInputSchema,
|
|
241
|
+
intent: 'read',
|
|
242
|
+
output: adapterCheckOutputSchema,
|
|
243
|
+
permit: 'public',
|
|
244
|
+
});
|
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `add.surface` trail -- Add a surface to an existing project.
|
|
3
3
|
*
|
|
4
|
-
* Generates
|
|
4
|
+
* Generates surface entry points and updates package.json dependencies.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync
|
|
8
|
-
import { basename,
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { basename, resolve } from 'node:path';
|
|
9
9
|
|
|
10
10
|
import { Result, trail } from '@ontrails/core';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
projectPathExists,
|
|
15
|
+
resolveProjectPath,
|
|
16
|
+
writeProjectFile,
|
|
17
|
+
} from '../project-writes.js';
|
|
18
|
+
import { ontrailsPackageRange } from '../versions.js';
|
|
13
19
|
import { findTopoPath } from './project.js';
|
|
20
|
+
import { stringifyScaffoldPackageJson } from './scaffold-json.js';
|
|
21
|
+
|
|
22
|
+
type Surface = 'cli' | 'http' | 'mcp';
|
|
14
23
|
|
|
15
24
|
const generateCliEntry = (appImportPath: string): string =>
|
|
16
|
-
`import {
|
|
25
|
+
`import { devPermitPreset, permitPreset } from '@ontrails/cli';
|
|
26
|
+
import { surface } from '@ontrails/commander';
|
|
17
27
|
|
|
18
28
|
import { app } from '${appImportPath}';
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
await surface(app, {
|
|
31
|
+
presets: [permitPreset(), devPermitPreset()],
|
|
32
|
+
});
|
|
21
33
|
`;
|
|
22
34
|
|
|
23
35
|
const generateMcpEntry = (appImportPath: string): string =>
|
|
24
|
-
`import {
|
|
36
|
+
`import { surface } from '@ontrails/mcp';
|
|
37
|
+
|
|
38
|
+
import { app } from '${appImportPath}';
|
|
39
|
+
|
|
40
|
+
await surface(app);
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const generateHttpEntry = (appImportPath: string): string =>
|
|
44
|
+
`import { surface } from '@ontrails/hono';
|
|
25
45
|
|
|
26
46
|
import { app } from '${appImportPath}';
|
|
27
47
|
|
|
28
|
-
await
|
|
48
|
+
await surface(app, { port: 3000 });
|
|
29
49
|
`;
|
|
30
50
|
|
|
51
|
+
const surfaceEntryFiles = {
|
|
52
|
+
cli: 'src/cli.ts',
|
|
53
|
+
http: 'src/http.ts',
|
|
54
|
+
mcp: 'src/mcp.ts',
|
|
55
|
+
} satisfies Record<Surface, string>;
|
|
56
|
+
|
|
57
|
+
const surfaceDependencies = {
|
|
58
|
+
cli: ['@ontrails/cli', '@ontrails/commander'],
|
|
59
|
+
http: ['@ontrails/hono', '@ontrails/http'],
|
|
60
|
+
mcp: ['@ontrails/mcp'],
|
|
61
|
+
} satisfies Record<Surface, readonly string[]>;
|
|
62
|
+
|
|
31
63
|
/** Resolve the entry file for a surface. */
|
|
32
|
-
const getEntryFile = (surface:
|
|
33
|
-
surface === 'cli' ? 'src/cli.ts' : 'src/mcp.ts';
|
|
64
|
+
const getEntryFile = (surface: Surface): string => surfaceEntryFiles[surface];
|
|
34
65
|
|
|
35
66
|
// ---------------------------------------------------------------------------
|
|
36
67
|
// Trail definition
|
|
@@ -39,14 +70,15 @@ const getEntryFile = (surface: 'cli' | 'mcp'): string =>
|
|
|
39
70
|
/** Patch deps and optionally bin in a parsed package.json. */
|
|
40
71
|
const patchPkgDeps = (
|
|
41
72
|
pkg: Record<string, unknown>,
|
|
42
|
-
surface:
|
|
73
|
+
surface: Surface,
|
|
43
74
|
cwd: string
|
|
44
75
|
): string => {
|
|
45
|
-
const depName =
|
|
76
|
+
const [depName = ''] = surfaceDependencies[surface];
|
|
46
77
|
const deps = (pkg['dependencies'] ?? {}) as Record<string, string>;
|
|
47
|
-
|
|
78
|
+
for (const dependency of surfaceDependencies[surface]) {
|
|
79
|
+
deps[dependency] = ontrailsPackageRange;
|
|
80
|
+
}
|
|
48
81
|
if (surface === 'cli') {
|
|
49
|
-
deps['commander'] = '^14.0.0';
|
|
50
82
|
pkg['bin'] = {
|
|
51
83
|
[(pkg['name'] as string | undefined) ?? basename(cwd)]: './src/cli.ts',
|
|
52
84
|
};
|
|
@@ -60,60 +92,82 @@ const patchPkgDeps = (
|
|
|
60
92
|
/** Update package.json with surface dependency and CLI bin if needed. */
|
|
61
93
|
const updatePkgJsonForSurface = async (
|
|
62
94
|
cwd: string,
|
|
63
|
-
surface:
|
|
64
|
-
): Promise<string
|
|
65
|
-
const
|
|
95
|
+
surface: Surface
|
|
96
|
+
): Promise<Result<string, Error>> => {
|
|
97
|
+
const pkgPathResult = resolveProjectPath(cwd, 'package.json');
|
|
98
|
+
if (pkgPathResult.isErr()) {
|
|
99
|
+
return Result.err(pkgPathResult.error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pkgPath = pkgPathResult.value;
|
|
66
103
|
if (!existsSync(pkgPath)) {
|
|
67
|
-
return surface
|
|
104
|
+
return Result.ok(surfaceDependencies[surface][0] ?? '');
|
|
68
105
|
}
|
|
69
106
|
const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
|
|
70
107
|
const depName = patchPkgDeps(pkg, surface, cwd);
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
const written = await writeProjectFile(
|
|
109
|
+
cwd,
|
|
110
|
+
'package.json',
|
|
111
|
+
stringifyScaffoldPackageJson(pkg)
|
|
112
|
+
);
|
|
113
|
+
return written.isErr() ? Result.err(written.error) : Result.ok(depName);
|
|
73
114
|
};
|
|
74
115
|
|
|
75
116
|
/** Create the entry file for a surface and return the relative path. */
|
|
76
117
|
const writeSurfaceEntry = async (
|
|
77
118
|
cwd: string,
|
|
78
|
-
surface:
|
|
79
|
-
): Promise<string
|
|
119
|
+
surface: Surface
|
|
120
|
+
): Promise<Result<string, Error>> => {
|
|
80
121
|
const entryFile = getEntryFile(surface);
|
|
81
|
-
const fullEntryPath = join(cwd, entryFile);
|
|
82
122
|
const appImport = (await findTopoPath(cwd)) ?? './app.js';
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
const generators = {
|
|
124
|
+
cli: generateCliEntry,
|
|
125
|
+
http: generateHttpEntry,
|
|
126
|
+
mcp: generateMcpEntry,
|
|
127
|
+
} satisfies Record<Surface, (appImportPath: string) => string>;
|
|
128
|
+
const content = generators[surface](appImport);
|
|
129
|
+
|
|
130
|
+
const written = await writeProjectFile(cwd, entryFile, content);
|
|
131
|
+
return written.isErr() ? Result.err(written.error) : Result.ok(entryFile);
|
|
91
132
|
};
|
|
92
133
|
|
|
93
134
|
export const addSurface = trail('add.surface', {
|
|
94
|
-
|
|
95
|
-
implementation: async (input) => {
|
|
135
|
+
blaze: async (input) => {
|
|
96
136
|
const cwd = resolve(input.dir ?? '.');
|
|
97
137
|
const { surface } = input;
|
|
98
138
|
const entryFile = getEntryFile(surface);
|
|
139
|
+
const entryExists = projectPathExists(cwd, entryFile);
|
|
140
|
+
if (entryExists.isErr()) {
|
|
141
|
+
return entryExists;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let created: string | null = null;
|
|
145
|
+
if (!entryExists.value) {
|
|
146
|
+
const written = await writeSurfaceEntry(cwd, surface);
|
|
147
|
+
if (written.isErr()) {
|
|
148
|
+
return written;
|
|
149
|
+
}
|
|
150
|
+
created = entryFile;
|
|
151
|
+
}
|
|
99
152
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
153
|
+
const dependency = await updatePkgJsonForSurface(cwd, surface);
|
|
154
|
+
if (dependency.isErr()) {
|
|
155
|
+
return dependency;
|
|
104
156
|
}
|
|
105
157
|
|
|
106
158
|
return Result.ok({
|
|
107
|
-
created
|
|
108
|
-
dependency:
|
|
159
|
+
created,
|
|
160
|
+
dependency: dependency.value,
|
|
109
161
|
});
|
|
110
162
|
},
|
|
163
|
+
description: 'Add a surface to an existing project',
|
|
111
164
|
input: z.object({
|
|
112
165
|
dir: z.string().optional().describe('Project directory'),
|
|
113
|
-
surface: z.enum(['cli', 'mcp']).describe('Surface to add'),
|
|
166
|
+
surface: z.enum(['cli', 'http', 'mcp']).describe('Surface to add'),
|
|
114
167
|
}),
|
|
115
168
|
output: z.object({
|
|
116
|
-
created: z.string(),
|
|
169
|
+
created: z.string().nullable(),
|
|
117
170
|
dependency: z.string(),
|
|
118
171
|
}),
|
|
172
|
+
permit: { scopes: ['project:write'] },
|
|
119
173
|
});
|
package/src/trails/add-trail.ts
CHANGED
|
@@ -2,58 +2,73 @@
|
|
|
2
2
|
* `add.trail` trail -- Scaffold a new trail file with tests.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
7
6
|
|
|
8
7
|
import { Result, trail } from '@ontrails/core';
|
|
9
8
|
import { z } from 'zod';
|
|
10
9
|
|
|
10
|
+
import {
|
|
11
|
+
trailIdToExportName,
|
|
12
|
+
trailIdToModuleName,
|
|
13
|
+
TRAIL_ID_MESSAGE,
|
|
14
|
+
TRAIL_ID_PATTERN,
|
|
15
|
+
validateTrailId,
|
|
16
|
+
writeProjectFile,
|
|
17
|
+
} from '../project-writes.js';
|
|
18
|
+
|
|
11
19
|
// ---------------------------------------------------------------------------
|
|
12
20
|
// Helpers
|
|
13
21
|
// ---------------------------------------------------------------------------
|
|
14
22
|
|
|
23
|
+
const literal = (value: string): string => JSON.stringify(value);
|
|
24
|
+
|
|
25
|
+
const deriveExampleMessage = (id: string): string => `${id} completed`;
|
|
26
|
+
|
|
15
27
|
const generateTrailFile = (
|
|
16
28
|
id: string,
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
description: string,
|
|
30
|
+
exampleName: string,
|
|
31
|
+
intent: 'read' | 'write' | 'destroy'
|
|
19
32
|
): string => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (destructive) {
|
|
25
|
-
markers.push(' destructive: true,');
|
|
26
|
-
}
|
|
27
|
-
const markerBlock = markers.length > 0 ? `\n${markers.join('\n')}` : '';
|
|
33
|
+
const intentLine =
|
|
34
|
+
intent === 'write' ? '' : `\n intent: ${literal(intent)},`;
|
|
35
|
+
const exampleMessage = deriveExampleMessage(id);
|
|
36
|
+
const trailName = trailIdToExportName(id);
|
|
28
37
|
|
|
29
38
|
return `import { Result, trail } from '@ontrails/core';
|
|
30
39
|
import { z } from 'zod';
|
|
31
40
|
|
|
32
|
-
export const ${
|
|
33
|
-
|
|
41
|
+
export const ${trailName} = trail(${literal(id)}, {
|
|
42
|
+
blaze: async () => {
|
|
43
|
+
return Result.ok({ message: ${literal(exampleMessage)} });
|
|
44
|
+
},
|
|
45
|
+
description: ${literal(description)},
|
|
34
46
|
examples: [
|
|
35
47
|
{
|
|
48
|
+
expected: { message: ${literal(exampleMessage)} },
|
|
36
49
|
input: {},
|
|
37
|
-
name:
|
|
50
|
+
name: ${literal(exampleName)},
|
|
38
51
|
},
|
|
39
52
|
],
|
|
40
|
-
|
|
41
|
-
return Result.ok({ message: 'TODO' });
|
|
42
|
-
},
|
|
43
|
-
input: z.object({}),${markerBlock}
|
|
53
|
+
input: z.object({}),${intentLine}
|
|
44
54
|
output: z.object({ message: z.string() }),
|
|
45
55
|
});
|
|
46
56
|
`;
|
|
47
57
|
};
|
|
48
58
|
|
|
49
|
-
const generateTestFile = (id: string): string => {
|
|
50
|
-
const moduleName = id
|
|
51
|
-
const trailName = id
|
|
59
|
+
const generateTestFile = (id: string, exampleName: string): string => {
|
|
60
|
+
const moduleName = trailIdToModuleName(id);
|
|
61
|
+
const trailName = trailIdToExportName(id);
|
|
62
|
+
const exampleMessage = deriveExampleMessage(id);
|
|
52
63
|
return `import { testTrail } from '@ontrails/testing';
|
|
53
64
|
import { ${trailName} } from '../src/trails/${moduleName}.js';
|
|
54
65
|
|
|
55
66
|
testTrail(${trailName}, [
|
|
56
|
-
{
|
|
67
|
+
{
|
|
68
|
+
description: ${literal(exampleName)},
|
|
69
|
+
expectValue: { message: ${literal(exampleMessage)} },
|
|
70
|
+
input: {},
|
|
71
|
+
},
|
|
57
72
|
]);
|
|
58
73
|
`;
|
|
59
74
|
};
|
|
@@ -62,42 +77,65 @@ testTrail(${trailName}, [
|
|
|
62
77
|
// Trail definition
|
|
63
78
|
// ---------------------------------------------------------------------------
|
|
64
79
|
|
|
65
|
-
/** Write a file, creating parent directories as needed. */
|
|
66
|
-
const writeWithDirs = async (
|
|
67
|
-
filePath: string,
|
|
68
|
-
content: string
|
|
69
|
-
): Promise<void> => {
|
|
70
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
71
|
-
await Bun.write(filePath, content);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
80
|
export const addTrail = trail('add.trail', {
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
args: ['id'],
|
|
82
|
+
blaze: async (input, ctx) => {
|
|
77
83
|
const { id } = input;
|
|
78
|
-
const
|
|
84
|
+
const validated = validateTrailId(id);
|
|
85
|
+
if (validated.isErr()) {
|
|
86
|
+
return validated;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const moduleName = trailIdToModuleName(validated.value);
|
|
79
90
|
const cwd = resolve(ctx.cwd ?? '.');
|
|
80
91
|
|
|
81
92
|
const files = new Map<string, string>([
|
|
82
93
|
[
|
|
83
94
|
`src/trails/${moduleName}.ts`,
|
|
84
|
-
generateTrailFile(
|
|
95
|
+
generateTrailFile(
|
|
96
|
+
id,
|
|
97
|
+
input.description,
|
|
98
|
+
input.exampleName,
|
|
99
|
+
input.intent
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
`__tests__/${moduleName}.test.ts`,
|
|
104
|
+
generateTestFile(id, input.exampleName),
|
|
85
105
|
],
|
|
86
|
-
[`__tests__/${moduleName}.test.ts`, generateTestFile(id)],
|
|
87
106
|
]);
|
|
88
107
|
|
|
89
108
|
for (const [relativePath, content] of files) {
|
|
90
|
-
await
|
|
109
|
+
const written = await writeProjectFile(cwd, relativePath, content);
|
|
110
|
+
if (written.isErr()) {
|
|
111
|
+
return written;
|
|
112
|
+
}
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
return Result.ok({ created: [...files.keys()] });
|
|
94
116
|
},
|
|
117
|
+
description: 'Scaffold a new trail with tests and examples',
|
|
95
118
|
input: z.object({
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
119
|
+
description: z
|
|
120
|
+
.string()
|
|
121
|
+
.min(1, 'Trail description is required')
|
|
122
|
+
.describe('Trail description'),
|
|
123
|
+
exampleName: z
|
|
124
|
+
.string()
|
|
125
|
+
.min(1, 'Starter example name is required')
|
|
126
|
+
.describe('Starter example name'),
|
|
127
|
+
id: z
|
|
128
|
+
.string()
|
|
129
|
+
.min(1, 'Trail ID is required')
|
|
130
|
+
.regex(TRAIL_ID_PATTERN, TRAIL_ID_MESSAGE)
|
|
131
|
+
.describe('Trail ID (e.g., entity.update)'),
|
|
132
|
+
intent: z
|
|
133
|
+
.enum(['read', 'write', 'destroy'])
|
|
134
|
+
.default('write')
|
|
135
|
+
.describe('Trail intent'),
|
|
99
136
|
}),
|
|
100
137
|
output: z.object({
|
|
101
138
|
created: z.array(z.string()),
|
|
102
139
|
}),
|
|
140
|
+
permit: { scopes: ['project:write'] },
|
|
103
141
|
});
|