@ontrails/trails 1.0.0-beta.4 → 1.0.0-beta.41

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +1104 -4
  2. package/README.md +27 -0
  3. package/package.json +31 -7
  4. package/src/app.ts +147 -3
  5. package/src/cli.ts +345 -11
  6. package/src/completions.ts +240 -0
  7. package/src/lifecycle-source-io.ts +33 -0
  8. package/src/load-app-mirror.ts +202 -0
  9. package/src/local-state-io.ts +173 -0
  10. package/src/mcp-app.ts +42 -0
  11. package/src/mcp-options.ts +91 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/config.ts +152 -0
  15. package/src/regrade/history.ts +430 -0
  16. package/src/regrade/live-api-preserve.ts +8 -0
  17. package/src/regrade/plan-artifact.ts +285 -0
  18. package/src/release/bindings.ts +39 -0
  19. package/src/release/check.ts +844 -0
  20. package/src/release/config.ts +63 -0
  21. package/src/release/contract-facts.ts +425 -0
  22. package/src/release/index.ts +142 -0
  23. package/src/release/lock-roundtrip-smoke.ts +230 -0
  24. package/src/release/native-bun-publish.ts +651 -0
  25. package/src/release/native-bun-registry.ts +765 -0
  26. package/src/release/notes-cli.ts +171 -0
  27. package/src/release/notes.ts +390 -0
  28. package/src/release/pack-coherence.ts +455 -0
  29. package/src/release/packed-artifacts-smoke.ts +236 -0
  30. package/src/release/policy.ts +1687 -0
  31. package/src/release/semver.ts +104 -0
  32. package/src/release/smoke.ts +56 -0
  33. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  34. package/src/retired-topo-command.ts +36 -0
  35. package/src/run-adapter-check.ts +76 -0
  36. package/src/run-collision.ts +126 -0
  37. package/src/run-completions-install.ts +179 -0
  38. package/src/run-example.ts +149 -0
  39. package/src/run-examples.ts +148 -0
  40. package/src/run-quiet.ts +75 -0
  41. package/src/run-release-check.ts +74 -0
  42. package/src/run-schema.ts +74 -0
  43. package/src/run-trace.ts +273 -0
  44. package/src/run-warden.ts +39 -0
  45. package/src/run-watch.ts +432 -0
  46. package/src/run-wayfind-outline.ts +170 -0
  47. package/src/scaffold-version-sync.ts +183 -0
  48. package/src/scaffold-versions.generated.ts +12 -0
  49. package/src/trails/adapter-check.ts +244 -0
  50. package/src/trails/add-surface.ts +99 -45
  51. package/src/trails/add-trail.ts +84 -37
  52. package/src/trails/add-verify.ts +100 -30
  53. package/src/trails/compile.ts +58 -0
  54. package/src/trails/completions-complete.ts +165 -0
  55. package/src/trails/completions.ts +47 -0
  56. package/src/trails/create-adapter.ts +785 -0
  57. package/src/trails/create-scaffold.ts +401 -106
  58. package/src/trails/create-versions.ts +62 -0
  59. package/src/trails/create.ts +186 -72
  60. package/src/trails/deprecate.ts +59 -0
  61. package/src/trails/dev-clean.ts +82 -0
  62. package/src/trails/dev-reset.ts +50 -0
  63. package/src/trails/dev-stats.ts +72 -0
  64. package/src/trails/dev-support.ts +360 -0
  65. package/src/trails/doctor.ts +77 -0
  66. package/src/trails/draft-promote.ts +949 -0
  67. package/src/trails/guide.ts +70 -68
  68. package/src/trails/load-app.ts +1123 -15
  69. package/src/trails/operator-context.ts +66 -0
  70. package/src/trails/project.ts +17 -3
  71. package/src/trails/regrade.ts +3091 -0
  72. package/src/trails/release-check.ts +105 -0
  73. package/src/trails/release-smoke.ts +49 -0
  74. package/src/trails/revise.ts +53 -0
  75. package/src/trails/root-dir.ts +21 -0
  76. package/src/trails/run-example.ts +475 -0
  77. package/src/trails/run-examples.ts +129 -0
  78. package/src/trails/run.ts +434 -0
  79. package/src/trails/scaffold-json.ts +58 -0
  80. package/src/trails/survey.ts +877 -214
  81. package/src/trails/topo-activation.ts +14 -0
  82. package/src/trails/topo-constants.ts +2 -0
  83. package/src/trails/topo-history.ts +47 -0
  84. package/src/trails/topo-output-schemas.ts +259 -0
  85. package/src/trails/topo-pin.ts +38 -0
  86. package/src/trails/topo-read-support.ts +368 -0
  87. package/src/trails/topo-reports.ts +809 -0
  88. package/src/trails/topo-store-support.ts +325 -0
  89. package/src/trails/topo-support.ts +220 -0
  90. package/src/trails/topo-unpin.ts +61 -0
  91. package/src/trails/topo.ts +92 -0
  92. package/src/trails/validate.ts +27 -0
  93. package/src/trails/version-lifecycle-support.ts +936 -0
  94. package/src/trails/warden-guide.ts +134 -0
  95. package/src/trails/warden.ts +198 -58
  96. package/src/trails/wayfind-outline.ts +876 -0
  97. package/src/trails/wayfind.ts +1053 -0
  98. package/src/versions.ts +31 -0
  99. package/.turbo/turbo-build.log +0 -1
  100. package/.turbo/turbo-lint.log +0 -3
  101. package/.turbo/turbo-typecheck.log +0 -1
  102. package/__tests__/examples.test.ts +0 -6
  103. package/dist/bin/trails.d.ts +0 -3
  104. package/dist/bin/trails.d.ts.map +0 -1
  105. package/dist/bin/trails.js +0 -4
  106. package/dist/bin/trails.js.map +0 -1
  107. package/dist/src/app.d.ts +0 -2
  108. package/dist/src/app.d.ts.map +0 -1
  109. package/dist/src/app.js +0 -11
  110. package/dist/src/app.js.map +0 -1
  111. package/dist/src/clack.d.ts +0 -9
  112. package/dist/src/clack.d.ts.map +0 -1
  113. package/dist/src/clack.js +0 -84
  114. package/dist/src/clack.js.map +0 -1
  115. package/dist/src/cli.d.ts +0 -2
  116. package/dist/src/cli.d.ts.map +0 -1
  117. package/dist/src/cli.js +0 -13
  118. package/dist/src/cli.js.map +0 -1
  119. package/dist/src/trails/add-surface.d.ts +0 -13
  120. package/dist/src/trails/add-surface.d.ts.map +0 -1
  121. package/dist/src/trails/add-surface.js +0 -88
  122. package/dist/src/trails/add-surface.js.map +0 -1
  123. package/dist/src/trails/add-trail.d.ts +0 -10
  124. package/dist/src/trails/add-trail.d.ts.map +0 -1
  125. package/dist/src/trails/add-trail.js +0 -77
  126. package/dist/src/trails/add-trail.js.map +0 -1
  127. package/dist/src/trails/add-verify.d.ts +0 -10
  128. package/dist/src/trails/add-verify.d.ts.map +0 -1
  129. package/dist/src/trails/add-verify.js +0 -67
  130. package/dist/src/trails/add-verify.js.map +0 -1
  131. package/dist/src/trails/create-scaffold.d.ts +0 -15
  132. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  133. package/dist/src/trails/create-scaffold.js +0 -288
  134. package/dist/src/trails/create-scaffold.js.map +0 -1
  135. package/dist/src/trails/create.d.ts +0 -22
  136. package/dist/src/trails/create.d.ts.map +0 -1
  137. package/dist/src/trails/create.js +0 -121
  138. package/dist/src/trails/create.js.map +0 -1
  139. package/dist/src/trails/guide.d.ts +0 -11
  140. package/dist/src/trails/guide.d.ts.map +0 -1
  141. package/dist/src/trails/guide.js +0 -80
  142. package/dist/src/trails/guide.js.map +0 -1
  143. package/dist/src/trails/load-app.d.ts +0 -4
  144. package/dist/src/trails/load-app.d.ts.map +0 -1
  145. package/dist/src/trails/load-app.js +0 -24
  146. package/dist/src/trails/load-app.js.map +0 -1
  147. package/dist/src/trails/project.d.ts +0 -8
  148. package/dist/src/trails/project.d.ts.map +0 -1
  149. package/dist/src/trails/project.js +0 -43
  150. package/dist/src/trails/project.js.map +0 -1
  151. package/dist/src/trails/survey.d.ts +0 -31
  152. package/dist/src/trails/survey.d.ts.map +0 -1
  153. package/dist/src/trails/survey.js +0 -221
  154. package/dist/src/trails/survey.js.map +0 -1
  155. package/dist/src/trails/warden.d.ts +0 -19
  156. package/dist/src/trails/warden.d.ts.map +0 -1
  157. package/dist/src/trails/warden.js +0 -88
  158. package/dist/src/trails/warden.js.map +0 -1
  159. package/dist/tsconfig.tsbuildinfo +0 -1
  160. package/src/__tests__/create.test.ts +0 -349
  161. package/src/__tests__/guide.test.ts +0 -91
  162. package/src/__tests__/load-app.test.ts +0 -15
  163. package/src/__tests__/survey.test.ts +0 -159
  164. package/src/__tests__/warden.test.ts +0 -74
  165. package/tsconfig.json +0 -9
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Scaffold dependency version derivation for the `create.versions` trail.
3
+ *
4
+ * Generates or validates `apps/trails/src/scaffold-versions.generated.ts`
5
+ * from the root `package.json` catalog and devDependencies, and validates
6
+ * that generated `@ontrails/*` package pins track the `@ontrails/trails`
7
+ * version exactly.
8
+ */
9
+
10
+ import { resolve } from 'node:path';
11
+
12
+ import {
13
+ ontrailsPackageRange as appOntrailsPackageRange,
14
+ trailsPackageVersion as appTrailsPackageVersion,
15
+ } from './versions.js';
16
+
17
+ interface RootPackageJson {
18
+ readonly catalog?: Record<string, string>;
19
+ readonly devDependencies?: Record<string, string>;
20
+ }
21
+
22
+ export interface OntrailsPackagePinState {
23
+ readonly ontrailsPackageRange?: string;
24
+ readonly trailsPackageVersion?: string;
25
+ }
26
+
27
+ export interface SyncScaffoldVersionsResult {
28
+ readonly generatedPath: string;
29
+ readonly mode: 'check' | 'write';
30
+ readonly written: boolean;
31
+ }
32
+
33
+ export const diagnoseOntrailsPackagePin = ({
34
+ ontrailsPackageRange,
35
+ trailsPackageVersion,
36
+ }: OntrailsPackagePinState): string | undefined => {
37
+ if (
38
+ typeof ontrailsPackageRange !== 'string' ||
39
+ typeof trailsPackageVersion !== 'string'
40
+ ) {
41
+ return (
42
+ 'create.versions: apps/trails/src/versions.ts must export ' +
43
+ '`ontrailsPackageRange` and `trailsPackageVersion`.'
44
+ );
45
+ }
46
+ if (ontrailsPackageRange !== trailsPackageVersion) {
47
+ return (
48
+ 'create.versions: scaffolded @ontrails/* packages must be exact ' +
49
+ `pins for @ontrails/trails (${trailsPackageVersion}); got ` +
50
+ `${ontrailsPackageRange}.`
51
+ );
52
+ }
53
+ return undefined;
54
+ };
55
+
56
+ const requireValue = (
57
+ value: string | undefined,
58
+ label: string,
59
+ source: string,
60
+ rootPackageJsonPath: string
61
+ ): string => {
62
+ if (typeof value !== 'string' || value.length === 0) {
63
+ throw new Error(
64
+ `create.versions: missing "${label}" under ${source} in ${rootPackageJsonPath}`
65
+ );
66
+ }
67
+ return value;
68
+ };
69
+
70
+ const loadScaffoldVersions = async (
71
+ rootPackageJsonPath: string
72
+ ): Promise<Record<string, string> & Readonly<Record<string, string>>> => {
73
+ const rootPkg = (await Bun.file(
74
+ rootPackageJsonPath
75
+ ).json()) as RootPackageJson;
76
+ const catalog = rootPkg.catalog ?? {};
77
+ const devDeps = rootPkg.devDependencies ?? {};
78
+
79
+ return {
80
+ bunTypes: requireValue(
81
+ devDeps['@types/bun'],
82
+ '@types/bun',
83
+ 'devDependencies',
84
+ rootPackageJsonPath
85
+ ),
86
+ commander: requireValue(
87
+ catalog['commander'],
88
+ 'commander',
89
+ 'catalog',
90
+ rootPackageJsonPath
91
+ ),
92
+ lefthook: requireValue(
93
+ devDeps['lefthook'],
94
+ 'lefthook',
95
+ 'devDependencies',
96
+ rootPackageJsonPath
97
+ ),
98
+ oxfmt: requireValue(
99
+ devDeps['oxfmt'],
100
+ 'oxfmt',
101
+ 'devDependencies',
102
+ rootPackageJsonPath
103
+ ),
104
+ oxlint: requireValue(
105
+ devDeps['oxlint'],
106
+ 'oxlint',
107
+ 'devDependencies',
108
+ rootPackageJsonPath
109
+ ),
110
+ typescript: requireValue(
111
+ devDeps['typescript'],
112
+ 'typescript',
113
+ 'devDependencies',
114
+ rootPackageJsonPath
115
+ ),
116
+ ultracite: requireValue(
117
+ devDeps['ultracite'],
118
+ 'ultracite',
119
+ 'devDependencies',
120
+ rootPackageJsonPath
121
+ ),
122
+ zod: requireValue(catalog['zod'], 'zod', 'catalog', rootPackageJsonPath),
123
+ };
124
+ };
125
+
126
+ const renderGeneratedFile = (
127
+ versions: Record<string, string> & Readonly<Record<string, string>>
128
+ ): string => {
129
+ const keys = Object.keys(versions).toSorted();
130
+ const lines = keys.map((key: string) => ` ${key}: '${versions[key]}',`);
131
+ return [
132
+ '// GENERATED FILE — do not edit by hand. Run `bun run scaffold-versions:sync` to regenerate.',
133
+ '',
134
+ 'export const scaffoldDependencyVersions = {',
135
+ ...lines,
136
+ '} as const;',
137
+ '',
138
+ ].join('\n');
139
+ };
140
+
141
+ export const syncScaffoldVersions = async (options: {
142
+ check: boolean;
143
+ rootDir: string;
144
+ }): Promise<SyncScaffoldVersionsResult> => {
145
+ const rootPackageJsonPath = resolve(options.rootDir, 'package.json');
146
+ const generatedPath = resolve(
147
+ options.rootDir,
148
+ 'apps/trails/src/scaffold-versions.generated.ts'
149
+ );
150
+ const versions = await loadScaffoldVersions(rootPackageJsonPath);
151
+ const expected = renderGeneratedFile(versions);
152
+
153
+ if (options.check) {
154
+ const generatedFile = Bun.file(generatedPath);
155
+ const existing = (await generatedFile.exists())
156
+ ? await generatedFile.text()
157
+ : undefined;
158
+ if (existing !== expected) {
159
+ throw new Error(
160
+ `create.versions: ${generatedPath} is out of date.\n` +
161
+ 'Run `bun run scaffold-versions:sync` to regenerate.'
162
+ );
163
+ }
164
+ } else {
165
+ await Bun.write(generatedPath, expected);
166
+ }
167
+
168
+ // Normally quiet because versions.ts derives both exports from one source;
169
+ // this trips only if future/manual drift breaks that invariant.
170
+ const diagnostic = diagnoseOntrailsPackagePin({
171
+ ontrailsPackageRange: appOntrailsPackageRange,
172
+ trailsPackageVersion: appTrailsPackageVersion,
173
+ });
174
+ if (diagnostic !== undefined) {
175
+ throw new Error(diagnostic);
176
+ }
177
+
178
+ return {
179
+ generatedPath,
180
+ mode: options.check ? 'check' : 'write',
181
+ written: !options.check,
182
+ };
183
+ };
@@ -0,0 +1,12 @@
1
+ // GENERATED FILE — do not edit by hand. Run `bun run scaffold-versions:sync` to regenerate.
2
+
3
+ export const scaffoldDependencyVersions = {
4
+ bunTypes: '^1.3.11',
5
+ commander: '^14.0.3',
6
+ lefthook: '^2.1.1',
7
+ oxfmt: '0.47.0',
8
+ oxlint: '1.62.0',
9
+ typescript: '^5.9.3',
10
+ ultracite: '7.6.2',
11
+ zod: '^4.3.5',
12
+ } as const;
@@ -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 manifest;
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
+ description: 'Check adapter authoring readiness',
212
+ examples: [
213
+ {
214
+ input: {},
215
+ name: 'Check adapters in the current workspace',
216
+ },
217
+ ],
218
+ implementation: (input, ctx) => {
219
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
220
+ if (rootDirResult.isErr()) {
221
+ return rootDirResult;
222
+ }
223
+
224
+ const rootDir = rootDirResult.value;
225
+ const validRoot = validateAdapterCheckRoot(rootDir);
226
+ if (validRoot.isErr()) {
227
+ return validRoot;
228
+ }
229
+
230
+ const report = checkAdapters(rootDir);
231
+
232
+ return Result.ok({
233
+ diagnostics: [...report.diagnostics],
234
+ formatted: formatAdapterCheckReport(report, rootDir),
235
+ passed: report.diagnostics.length === 0,
236
+ subjects: [...report.subjects],
237
+ targets: [...report.targets],
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 the CLI or MCP entry point and updates package.json dependencies.
4
+ * Generates surface entry points and updates package.json dependencies.
5
5
  */
6
6
 
7
- import { existsSync, mkdirSync } from 'node:fs';
8
- import { basename, dirname, join, resolve } from 'node:path';
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 { blaze } from '@ontrails/cli/commander';
25
+ `import { devPermitPreset, permitPreset } from '@ontrails/cli';
26
+ import { surface } from '@ontrails/commander';
17
27
 
18
28
  import { app } from '${appImportPath}';
19
29
 
20
- blaze(app);
30
+ await surface(app, {
31
+ presets: [permitPreset(), devPermitPreset()],
32
+ });
21
33
  `;
22
34
 
23
35
  const generateMcpEntry = (appImportPath: string): string =>
24
- `import { blaze } from '@ontrails/mcp';
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 blaze(app);
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: 'cli' | 'mcp'): string =>
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: 'cli' | 'mcp',
73
+ surface: Surface,
43
74
  cwd: string
44
75
  ): string => {
45
- const depName = surface === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
76
+ const [depName = ''] = surfaceDependencies[surface];
46
77
  const deps = (pkg['dependencies'] ?? {}) as Record<string, string>;
47
- deps[depName] = 'workspace:*';
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: 'cli' | 'mcp'
64
- ): Promise<string> => {
65
- const pkgPath = join(cwd, 'package.json');
95
+ surface: Surface
96
+ ): Promise<Result<string, Error>> => {
97
+ const pkgPathResult = resolveProjectPath(cwd, 'package.json');
98
+ if (pkgPathResult.isErr()) {
99
+ return pkgPathResult;
100
+ }
101
+
102
+ const pkgPath = pkgPathResult.value;
66
103
  if (!existsSync(pkgPath)) {
67
- return surface === 'cli' ? '@ontrails/cli' : '@ontrails/mcp';
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
- await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
72
- return depName;
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: 'cli' | 'mcp'
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 content =
84
- surface === 'cli'
85
- ? generateCliEntry(appImport)
86
- : generateMcpEntry(appImport);
87
-
88
- mkdirSync(dirname(fullEntryPath), { recursive: true });
89
- await Bun.write(fullEntryPath, content);
90
- return entryFile;
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
135
  description: 'Add a surface to an existing project',
95
- input: z.object({
96
- dir: z.string().optional().describe('Project directory'),
97
- surface: z.enum(['cli', 'mcp']).describe('Surface to add'),
98
- }),
99
- output: z.object({
100
- created: z.string(),
101
- dependency: z.string(),
102
- }),
103
- run: async (input) => {
136
+ implementation: async (input) => {
104
137
  const cwd = resolve(input.dir ?? '.');
105
138
  const { surface } = input;
106
139
  const entryFile = getEntryFile(surface);
140
+ const entryExists = projectPathExists(cwd, entryFile);
141
+ if (entryExists.isErr()) {
142
+ return entryExists;
143
+ }
144
+
145
+ let created: string | null = null;
146
+ if (!entryExists.value) {
147
+ const written = await writeSurfaceEntry(cwd, surface);
148
+ if (written.isErr()) {
149
+ return written;
150
+ }
151
+ created = entryFile;
152
+ }
107
153
 
108
- if (existsSync(join(cwd, entryFile))) {
109
- return Result.err(
110
- new Error(`${surface.toUpperCase()} is already blazed. Nothing to do.`)
111
- );
154
+ const dependency = await updatePkgJsonForSurface(cwd, surface);
155
+ if (dependency.isErr()) {
156
+ return dependency;
112
157
  }
113
158
 
114
159
  return Result.ok({
115
- created: await writeSurfaceEntry(cwd, surface),
116
- dependency: await updatePkgJsonForSurface(cwd, surface),
160
+ created,
161
+ dependency: dependency.value,
117
162
  });
118
163
  },
164
+ input: z.object({
165
+ dir: z.string().optional().describe('Project directory'),
166
+ surface: z.enum(['cli', 'http', 'mcp']).describe('Surface to add'),
167
+ }),
168
+ output: z.object({
169
+ created: z.string().nullable(),
170
+ dependency: z.string(),
171
+ }),
172
+ permit: { scopes: ['project:write'] },
119
173
  });