@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,91 @@
1
+ import type {
2
+ CreateServerOptions,
3
+ McpSurfaceTrailheadMap,
4
+ } from '@ontrails/mcp';
5
+
6
+ import { trailsOverlays } from './app.js';
7
+
8
+ export const trailsMcpIncludedTrails = [
9
+ 'adapter.check',
10
+ 'add.surface',
11
+ 'add.trail',
12
+ 'compile',
13
+ 'create',
14
+ 'create.adapter',
15
+ 'deprecate',
16
+ 'dev.clean',
17
+ 'dev.reset',
18
+ 'dev.stats',
19
+ 'doctor',
20
+ 'draft.promote',
21
+ 'guide',
22
+ 'adjust.regrade',
23
+ 'apply.regrade',
24
+ 'check.regrade',
25
+ 'list.regrades',
26
+ 'plan.regrade',
27
+ 'preview.regrade',
28
+ 'release.check',
29
+ 'release.smoke',
30
+ 'revise',
31
+ 'run',
32
+ 'run.example',
33
+ 'run.examples',
34
+ 'survey',
35
+ 'survey.brief',
36
+ 'survey.diff',
37
+ 'survey.resource',
38
+ 'survey.signal',
39
+ 'survey.surfaces',
40
+ 'survey.trail',
41
+ 'topo',
42
+ 'topo.history',
43
+ 'topo.pin',
44
+ 'topo.unpin',
45
+ 'validate',
46
+ 'warden',
47
+ 'warden.guide',
48
+ 'wayfind.adapters',
49
+ 'wayfind.contract',
50
+ 'wayfind.diff',
51
+ 'wayfind.errors',
52
+ 'wayfind.examples',
53
+ 'wayfind.impact',
54
+ 'wayfind.nearby',
55
+ 'wayfind.overview',
56
+ 'wayfind.search',
57
+ 'wayfind.trails',
58
+ ] as const;
59
+
60
+ export const trailsMcpTrailheads = {
61
+ inspect: {
62
+ description:
63
+ 'Inspect saved topo structure, resources, signals, surfaces, and diffs.',
64
+ mcp: { loading: 'deferred' },
65
+ trails: [
66
+ 'survey',
67
+ 'topo',
68
+ 'guide',
69
+ 'survey.brief',
70
+ 'survey.diff',
71
+ 'survey.resource',
72
+ 'survey.signal',
73
+ 'survey.surfaces',
74
+ 'survey.trail',
75
+ 'topo.history',
76
+ ],
77
+ },
78
+ } satisfies McpSurfaceTrailheadMap;
79
+
80
+ export const trailsMcpSurfaceOptions = {
81
+ description:
82
+ 'Trails framework operator surface. Use MCP resources for cold context, direct tools for high-signal work, and the inspect trailhead for saved topo reads.',
83
+ include: trailsMcpIncludedTrails,
84
+ mcpResources: { examples: true, graph: true, surfaceMap: true },
85
+ name: 'trails',
86
+ // The overlay authors the lockable `inspect` default; the call-site map
87
+ // below is the runtime override-in-context with richer metadata
88
+ // (description, deferred loading) over the same member selectors.
89
+ overlays: trailsOverlays,
90
+ trailheads: trailsMcpTrailheads,
91
+ } satisfies CreateServerOptions;
package/src/mcp.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bun
2
+ /* oxlint-disable eslint-plugin-jest/require-hook -- MCP stdio entrypoints execute at module scope */
3
+ import { surface } from '@ontrails/mcp';
4
+
5
+ import { trailsMcpApp } from './mcp-app.js';
6
+ import { trailsMcpSurfaceOptions } from './mcp-options.js';
7
+
8
+ await surface(trailsMcpApp, trailsMcpSurfaceOptions);
@@ -0,0 +1,377 @@
1
+ import { existsSync, mkdirSync, renameSync } from 'node:fs';
2
+ import { dirname, relative, resolve } from 'node:path';
3
+
4
+ import {
5
+ DRAFT_ID_PREFIX,
6
+ deriveSafePath,
7
+ InternalError,
8
+ Result,
9
+ ValidationError,
10
+ } from '@ontrails/core';
11
+ import type { Result as TrailsResult } from '@ontrails/core';
12
+
13
+ export const PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u;
14
+ export const PROJECT_NAME_MESSAGE =
15
+ 'Project name must start with a lowercase letter or digit and contain only lowercase letters, digits, ".", "_", or "-".';
16
+
17
+ export const TRAIL_ID_PATTERN =
18
+ /^(?:_draft\.)?[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)*$/u;
19
+ export const TRAIL_ID_MESSAGE =
20
+ 'Trail ID must be lowercase dotted segments, optionally prefixed with "_draft.", with each non-draft segment starting with a letter and containing only letters or digits.';
21
+
22
+ const asError = (error: unknown): Error =>
23
+ error instanceof Error ? error : new Error(String(error));
24
+
25
+ export type PlannedProjectOperation =
26
+ | { readonly kind: 'mkdir'; readonly path: string }
27
+ | { readonly kind: 'rename'; readonly from: string; readonly to: string }
28
+ | { readonly kind: 'write'; readonly path: string };
29
+
30
+ export type ProjectWriteOperation =
31
+ | { readonly kind: 'mkdir'; readonly path: string }
32
+ | { readonly kind: 'rename'; readonly from: string; readonly to: string }
33
+ | {
34
+ readonly content: string | Uint8Array;
35
+ readonly kind: 'write';
36
+ readonly path: string;
37
+ };
38
+
39
+ interface ProjectOperationOptions {
40
+ readonly existing?: 'overwrite' | 'preserve';
41
+ }
42
+
43
+ export const validateProjectName = (
44
+ name: string
45
+ ): TrailsResult<string, ValidationError> =>
46
+ PROJECT_NAME_PATTERN.test(name)
47
+ ? Result.ok(name)
48
+ : Result.err(new ValidationError(PROJECT_NAME_MESSAGE));
49
+
50
+ export const validateTrailId = (
51
+ trailId: string
52
+ ): TrailsResult<string, ValidationError> =>
53
+ TRAIL_ID_PATTERN.test(trailId)
54
+ ? Result.ok(trailId)
55
+ : Result.err(new ValidationError(TRAIL_ID_MESSAGE));
56
+
57
+ export const trailIdToModuleName = (trailId: string): string =>
58
+ trailId.startsWith(DRAFT_ID_PREFIX)
59
+ ? `${DRAFT_ID_PREFIX}${trailId.slice(DRAFT_ID_PREFIX.length).replaceAll('.', '-')}`
60
+ : trailId.replaceAll('.', '-');
61
+
62
+ export const trailIdToExportName = (trailId: string): string =>
63
+ trailId.replaceAll('.', '_');
64
+
65
+ export const resolveProjectDir = (
66
+ parentDir: string,
67
+ projectName: string
68
+ ): TrailsResult<string, Error> => {
69
+ const validated = validateProjectName(projectName);
70
+ if (validated.isErr()) {
71
+ return validated;
72
+ }
73
+
74
+ return deriveSafePath(resolve(parentDir), validated.value);
75
+ };
76
+
77
+ export const resolveProjectPath = (
78
+ projectDir: string,
79
+ relativePath: string
80
+ ): TrailsResult<string, Error> => deriveSafePath(projectDir, relativePath);
81
+
82
+ export const projectPathExists = (
83
+ projectDir: string,
84
+ pathWithinProject: string
85
+ ): TrailsResult<boolean, Error> => {
86
+ const target = resolveProjectPath(projectDir, pathWithinProject);
87
+ if (target.isErr()) {
88
+ return target;
89
+ }
90
+
91
+ return Result.ok(existsSync(target.value));
92
+ };
93
+
94
+ /** Write a generated project-relative file and return the relative path. */
95
+ export const writeProjectFile = async (
96
+ projectDir: string,
97
+ relativePath: string,
98
+ content: string | Uint8Array
99
+ ): Promise<TrailsResult<string, Error>> => {
100
+ const target = resolveProjectPath(projectDir, relativePath);
101
+ if (target.isErr()) {
102
+ return target;
103
+ }
104
+
105
+ try {
106
+ mkdirSync(dirname(target.value), { recursive: true });
107
+ await Bun.write(target.value, content);
108
+ return Result.ok(relativePath);
109
+ } catch (error) {
110
+ return Result.err(
111
+ new InternalError(`Failed to write project file "${relativePath}"`, {
112
+ cause: asError(error),
113
+ context: { projectDir, relativePath },
114
+ })
115
+ );
116
+ }
117
+ };
118
+
119
+ /** Write an already-derived path that must stay contained under the project. */
120
+ export const writeContainedProjectPath = async (
121
+ projectDir: string,
122
+ pathWithinProject: string,
123
+ content: string | Uint8Array
124
+ ): Promise<TrailsResult<string, Error>> => {
125
+ const target = resolveProjectPath(projectDir, pathWithinProject);
126
+ if (target.isErr()) {
127
+ return target;
128
+ }
129
+
130
+ try {
131
+ mkdirSync(dirname(target.value), { recursive: true });
132
+ await Bun.write(target.value, content);
133
+ return Result.ok(target.value);
134
+ } catch (error) {
135
+ return Result.err(
136
+ new InternalError(
137
+ `Failed to write contained project path "${pathWithinProject}"`,
138
+ {
139
+ cause: asError(error),
140
+ context: { pathWithinProject, projectDir },
141
+ }
142
+ )
143
+ );
144
+ }
145
+ };
146
+
147
+ export const renameContainedProjectPath = (
148
+ projectDir: string,
149
+ fromPath: string,
150
+ toPath: string
151
+ ): TrailsResult<void, Error> => {
152
+ const from = resolveProjectPath(projectDir, fromPath);
153
+ if (from.isErr()) {
154
+ return from;
155
+ }
156
+
157
+ const to = resolveProjectPath(projectDir, toPath);
158
+ if (to.isErr()) {
159
+ return to;
160
+ }
161
+
162
+ try {
163
+ renameSync(from.value, to.value);
164
+ return Result.ok();
165
+ } catch (error) {
166
+ return Result.err(
167
+ new InternalError(
168
+ `Failed to rename contained project path "${fromPath}"`,
169
+ {
170
+ cause: asError(error),
171
+ context: { fromPath, projectDir, toPath },
172
+ }
173
+ )
174
+ );
175
+ }
176
+ };
177
+
178
+ const toProjectRelativePath = (
179
+ projectDir: string,
180
+ pathWithinProject: string
181
+ ): TrailsResult<string, Error> => {
182
+ const target = resolveProjectPath(projectDir, pathWithinProject);
183
+ if (target.isErr()) {
184
+ return target;
185
+ }
186
+
187
+ return Result.ok(
188
+ relative(resolve(projectDir), target.value).replaceAll('\\', '/')
189
+ );
190
+ };
191
+
192
+ export const planProjectOperation = (
193
+ projectDir: string,
194
+ operation: ProjectWriteOperation
195
+ ): TrailsResult<PlannedProjectOperation, Error> => {
196
+ switch (operation.kind) {
197
+ case 'mkdir': {
198
+ const path = toProjectRelativePath(projectDir, operation.path);
199
+ return path.isErr()
200
+ ? path
201
+ : Result.ok({ kind: 'mkdir', path: path.value });
202
+ }
203
+ case 'rename': {
204
+ const from = toProjectRelativePath(projectDir, operation.from);
205
+ if (from.isErr()) {
206
+ return from;
207
+ }
208
+ const to = toProjectRelativePath(projectDir, operation.to);
209
+ return to.isErr()
210
+ ? to
211
+ : Result.ok({ from: from.value, kind: 'rename', to: to.value });
212
+ }
213
+ case 'write': {
214
+ const path = toProjectRelativePath(projectDir, operation.path);
215
+ return path.isErr()
216
+ ? path
217
+ : Result.ok({ kind: 'write', path: path.value });
218
+ }
219
+ default: {
220
+ return Result.err(
221
+ new InternalError('Unknown project operation kind', {
222
+ context: { operation },
223
+ })
224
+ );
225
+ }
226
+ }
227
+ };
228
+
229
+ const shouldApplyProjectOperation = (
230
+ projectDir: string,
231
+ operation: ProjectWriteOperation,
232
+ options: ProjectOperationOptions
233
+ ): TrailsResult<boolean, Error> => {
234
+ if (options.existing !== 'preserve' || operation.kind === 'rename') {
235
+ return Result.ok(true);
236
+ }
237
+
238
+ const { path } = operation;
239
+ const target = resolveProjectPath(projectDir, path);
240
+ if (target.isErr()) {
241
+ return target;
242
+ }
243
+
244
+ return Result.ok(!existsSync(target.value));
245
+ };
246
+
247
+ const selectProjectOperations = (
248
+ projectDir: string,
249
+ operations: readonly ProjectWriteOperation[],
250
+ options: ProjectOperationOptions
251
+ ): TrailsResult<ProjectWriteOperation[], Error> => {
252
+ const selected: ProjectWriteOperation[] = [];
253
+ for (const operation of operations) {
254
+ const shouldApply = shouldApplyProjectOperation(
255
+ projectDir,
256
+ operation,
257
+ options
258
+ );
259
+ if (shouldApply.isErr()) {
260
+ return shouldApply;
261
+ }
262
+ if (shouldApply.value) {
263
+ selected.push(operation);
264
+ }
265
+ }
266
+
267
+ return Result.ok(selected);
268
+ };
269
+
270
+ export const planProjectOperations = (
271
+ projectDir: string,
272
+ operations: readonly ProjectWriteOperation[],
273
+ options: ProjectOperationOptions = {}
274
+ ): TrailsResult<PlannedProjectOperation[], Error> => {
275
+ const selected = selectProjectOperations(projectDir, operations, options);
276
+ if (selected.isErr()) {
277
+ return selected;
278
+ }
279
+
280
+ const planned: PlannedProjectOperation[] = [];
281
+ for (const operation of selected.value) {
282
+ const result = planProjectOperation(projectDir, operation);
283
+ if (result.isErr()) {
284
+ return result;
285
+ }
286
+ planned.push(result.value);
287
+ }
288
+ return Result.ok(planned);
289
+ };
290
+
291
+ const applyProjectOperation = async (
292
+ projectDir: string,
293
+ operation: ProjectWriteOperation
294
+ ): Promise<TrailsResult<void, Error>> => {
295
+ switch (operation.kind) {
296
+ case 'mkdir': {
297
+ const target = resolveProjectPath(projectDir, operation.path);
298
+ if (target.isErr()) {
299
+ return target;
300
+ }
301
+ try {
302
+ mkdirSync(target.value, { recursive: true });
303
+ return Result.ok();
304
+ } catch (error) {
305
+ return Result.err(
306
+ new InternalError(
307
+ `Failed to create project directory "${operation.path}"`,
308
+ {
309
+ cause: asError(error),
310
+ context: { projectDir, relativePath: operation.path },
311
+ }
312
+ )
313
+ );
314
+ }
315
+ }
316
+ case 'rename': {
317
+ return renameContainedProjectPath(
318
+ projectDir,
319
+ operation.from,
320
+ operation.to
321
+ );
322
+ }
323
+ case 'write': {
324
+ const target = resolveProjectPath(projectDir, operation.path);
325
+ if (target.isErr()) {
326
+ return target;
327
+ }
328
+ try {
329
+ mkdirSync(dirname(target.value), { recursive: true });
330
+ await Bun.write(target.value, operation.content);
331
+ return Result.ok();
332
+ } catch (error) {
333
+ return Result.err(
334
+ new InternalError(
335
+ `Failed to write project file "${operation.path}"`,
336
+ {
337
+ cause: asError(error),
338
+ context: { projectDir, relativePath: operation.path },
339
+ }
340
+ )
341
+ );
342
+ }
343
+ }
344
+ default: {
345
+ return Result.err(
346
+ new InternalError('Unknown project operation kind', {
347
+ context: { operation },
348
+ })
349
+ );
350
+ }
351
+ }
352
+ };
353
+
354
+ export const applyProjectOperations = async (
355
+ projectDir: string,
356
+ operations: readonly ProjectWriteOperation[],
357
+ options: ProjectOperationOptions = {}
358
+ ): Promise<TrailsResult<PlannedProjectOperation[], Error>> => {
359
+ const selected = selectProjectOperations(projectDir, operations, options);
360
+ if (selected.isErr()) {
361
+ return selected;
362
+ }
363
+
364
+ const planned = planProjectOperations(projectDir, selected.value);
365
+ if (planned.isErr()) {
366
+ return planned;
367
+ }
368
+
369
+ for (const operation of selected.value) {
370
+ const applied = await applyProjectOperation(projectDir, operation);
371
+ if (applied.isErr()) {
372
+ return applied;
373
+ }
374
+ }
375
+
376
+ return planned;
377
+ };
@@ -0,0 +1,152 @@
1
+ import { loadTrailsConfigValue } from '@ontrails/config';
2
+ import {
3
+ InternalError,
4
+ Result,
5
+ ValidationError,
6
+ pathScopeSchema,
7
+ } from '@ontrails/core';
8
+ import type { Result as TrailsResult } from '@ontrails/core';
9
+ import { z } from 'zod';
10
+
11
+ export const regradeConfigSchema = z
12
+ .object({
13
+ scope: pathScopeSchema.optional(),
14
+ })
15
+ .default({});
16
+
17
+ export type RegradeConfig = z.output<typeof regradeConfigSchema>;
18
+
19
+ interface RegradeConfigLoadResult {
20
+ readonly config?: RegradeConfig;
21
+ readonly configPath?: string;
22
+ }
23
+
24
+ interface ResolvableConfig {
25
+ readonly resolve: (options?: {
26
+ readonly cwd?: string;
27
+ readonly env?: Record<string, string | undefined>;
28
+ }) => unknown;
29
+ }
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
32
+ typeof value === 'object' && value !== null && !Array.isArray(value);
33
+
34
+ const isResultLike = (
35
+ value: unknown
36
+ ): value is {
37
+ readonly error?: unknown;
38
+ readonly isErr: () => boolean;
39
+ readonly isOk: () => boolean;
40
+ readonly value?: unknown;
41
+ } =>
42
+ isRecord(value) &&
43
+ typeof value['isOk'] === 'function' &&
44
+ typeof value['isErr'] === 'function';
45
+
46
+ const isResolvableConfig = (value: unknown): value is ResolvableConfig =>
47
+ isRecord(value) && typeof value['resolve'] === 'function';
48
+
49
+ const errorMessage = (error: unknown): string =>
50
+ error instanceof Error ? error.message : String(error);
51
+
52
+ const extractRegradeConfig = (
53
+ value: unknown
54
+ ): TrailsResult<RegradeConfig | undefined, ValidationError> => {
55
+ if (!(isRecord(value) && 'regrade' in value)) {
56
+ return Result.ok();
57
+ }
58
+
59
+ const parsed = regradeConfigSchema.safeParse(value['regrade']);
60
+ if (parsed.success) {
61
+ return Result.ok(parsed.data);
62
+ }
63
+
64
+ return Result.err(
65
+ new ValidationError('Invalid regrade config in Trails config file.', {
66
+ context: { issues: parsed.error.issues },
67
+ })
68
+ );
69
+ };
70
+
71
+ export const loadRegradeConfig = async ({
72
+ configPath,
73
+ env = {},
74
+ rootDir,
75
+ }: {
76
+ readonly configPath?: string | undefined;
77
+ readonly env?: Record<string, string | undefined> | undefined;
78
+ readonly rootDir: string;
79
+ }): Promise<
80
+ TrailsResult<RegradeConfigLoadResult, InternalError | ValidationError>
81
+ > => {
82
+ try {
83
+ const loaded = await loadTrailsConfigValue({
84
+ configPath,
85
+ rootDir,
86
+ });
87
+ const exported = loaded.value;
88
+ if (exported === undefined) {
89
+ return Result.ok({});
90
+ }
91
+
92
+ if (isResolvableConfig(exported)) {
93
+ const resolved = await exported.resolve({ cwd: rootDir, env });
94
+ if (isResultLike(resolved)) {
95
+ if (resolved.isErr()) {
96
+ return Result.err(
97
+ new InternalError(
98
+ `Failed to resolve regrade config: ${errorMessage(resolved.error)}`
99
+ )
100
+ );
101
+ }
102
+ const configResult = extractRegradeConfig(resolved.value);
103
+ if (configResult.isErr()) {
104
+ return configResult;
105
+ }
106
+ return Result.ok({
107
+ ...(configResult.value === undefined
108
+ ? {}
109
+ : { config: configResult.value }),
110
+ ...(loaded.configPath === undefined
111
+ ? {}
112
+ : { configPath: loaded.configPath }),
113
+ });
114
+ }
115
+
116
+ const configResult = extractRegradeConfig(resolved);
117
+ if (configResult.isErr()) {
118
+ return configResult;
119
+ }
120
+ return Result.ok({
121
+ ...(configResult.value === undefined
122
+ ? {}
123
+ : { config: configResult.value }),
124
+ ...(loaded.configPath === undefined
125
+ ? {}
126
+ : { configPath: loaded.configPath }),
127
+ });
128
+ }
129
+
130
+ const configResult = extractRegradeConfig(exported);
131
+ if (configResult.isErr()) {
132
+ return configResult;
133
+ }
134
+ return Result.ok({
135
+ ...(configResult.value === undefined
136
+ ? {}
137
+ : { config: configResult.value }),
138
+ ...(loaded.configPath === undefined
139
+ ? {}
140
+ : { configPath: loaded.configPath }),
141
+ });
142
+ } catch (error) {
143
+ return Result.err(
144
+ new InternalError(
145
+ `Failed to load regrade config: ${errorMessage(error)}`,
146
+ {
147
+ cause: error instanceof Error ? error : new Error(String(error)),
148
+ }
149
+ )
150
+ );
151
+ }
152
+ };