@ontrails/trails 1.0.0-beta.2 → 1.0.0-beta.21
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 +628 -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 +93 -40
- package/src/trails/add-trail.ts +79 -41
- package/src/trails/add-verify.ts +94 -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 +354 -74
- 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/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
|
@@ -4,12 +4,28 @@
|
|
|
4
4
|
* Generates package.json, tsconfig, app.ts, starter trails, and .trails/ directory.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
9
8
|
|
|
10
|
-
import { Result, trail } from '@ontrails/core';
|
|
9
|
+
import { Result, trail, WORKSPACE_GITIGNORE_CONTENT } from '@ontrails/core';
|
|
11
10
|
import { z } from 'zod';
|
|
12
11
|
|
|
12
|
+
import {
|
|
13
|
+
applyProjectOperations,
|
|
14
|
+
planProjectOperations,
|
|
15
|
+
PROJECT_NAME_MESSAGE,
|
|
16
|
+
PROJECT_NAME_PATTERN,
|
|
17
|
+
resolveProjectDir,
|
|
18
|
+
} from '../project-writes.js';
|
|
19
|
+
import type {
|
|
20
|
+
PlannedProjectOperation,
|
|
21
|
+
ProjectWriteOperation,
|
|
22
|
+
} from '../project-writes.js';
|
|
23
|
+
import {
|
|
24
|
+
ontrailsPackageRange,
|
|
25
|
+
scaffoldDependencyVersions,
|
|
26
|
+
trailsPackageVersion,
|
|
27
|
+
} from '../versions.js';
|
|
28
|
+
|
|
13
29
|
// ---------------------------------------------------------------------------
|
|
14
30
|
// Types
|
|
15
31
|
// ---------------------------------------------------------------------------
|
|
@@ -19,30 +35,63 @@ type Starter = 'empty' | 'entity' | 'hello';
|
|
|
19
35
|
interface ScaffoldResult {
|
|
20
36
|
readonly created: string[];
|
|
21
37
|
readonly dir: string;
|
|
38
|
+
readonly dryRun: boolean;
|
|
22
39
|
readonly name: string;
|
|
40
|
+
readonly plannedOperations: PlannedProjectOperation[];
|
|
23
41
|
}
|
|
24
42
|
|
|
43
|
+
const frameworkCommandScripts = {
|
|
44
|
+
add: 'trails add',
|
|
45
|
+
compile: 'trails compile',
|
|
46
|
+
completions: 'trails completions',
|
|
47
|
+
deprecate: 'trails deprecate',
|
|
48
|
+
diff: 'trails diff',
|
|
49
|
+
doctor: 'trails doctor',
|
|
50
|
+
guide: 'trails guide',
|
|
51
|
+
revise: 'trails revise',
|
|
52
|
+
run: 'trails run',
|
|
53
|
+
survey: 'trails survey',
|
|
54
|
+
topo: 'trails topo',
|
|
55
|
+
validate: 'trails validate',
|
|
56
|
+
warden: 'trails warden',
|
|
57
|
+
} as const satisfies Record<string, string>;
|
|
58
|
+
|
|
25
59
|
// ---------------------------------------------------------------------------
|
|
26
60
|
// Content generators
|
|
27
61
|
// ---------------------------------------------------------------------------
|
|
28
62
|
|
|
29
63
|
const generatePackageJson = (name: string): string => {
|
|
30
64
|
const deps: Record<string, string> = {
|
|
31
|
-
'@ontrails/core':
|
|
32
|
-
zod:
|
|
65
|
+
'@ontrails/core': ontrailsPackageRange,
|
|
66
|
+
zod: scaffoldDependencyVersions.zod,
|
|
33
67
|
};
|
|
34
68
|
|
|
35
69
|
const pkg: Record<string, unknown> = {
|
|
36
70
|
dependencies: Object.fromEntries(
|
|
37
71
|
Object.entries(deps).toSorted(([a], [b]) => a.localeCompare(b))
|
|
38
72
|
),
|
|
73
|
+
devDependencies: Object.fromEntries(
|
|
74
|
+
Object.entries({
|
|
75
|
+
'@ontrails/trails': ontrailsPackageRange,
|
|
76
|
+
'@types/bun': scaffoldDependencyVersions.bunTypes,
|
|
77
|
+
oxfmt: scaffoldDependencyVersions.oxfmt,
|
|
78
|
+
oxlint: scaffoldDependencyVersions.oxlint,
|
|
79
|
+
typescript: scaffoldDependencyVersions.typescript,
|
|
80
|
+
ultracite: scaffoldDependencyVersions.ultracite,
|
|
81
|
+
}).toSorted(([a], [b]) => a.localeCompare(b))
|
|
82
|
+
),
|
|
39
83
|
name,
|
|
40
|
-
scripts:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
scripts: Object.fromEntries(
|
|
85
|
+
Object.entries({
|
|
86
|
+
build: 'tsc -b',
|
|
87
|
+
'format:check': 'bunx ultracite check .',
|
|
88
|
+
'format:fix': 'bunx ultracite fix .',
|
|
89
|
+
lint: 'oxlint ./src',
|
|
90
|
+
test: 'bun test',
|
|
91
|
+
typecheck: 'tsc --noEmit',
|
|
92
|
+
...frameworkCommandScripts,
|
|
93
|
+
}).toSorted(([a], [b]) => a.localeCompare(b))
|
|
94
|
+
),
|
|
46
95
|
type: 'module',
|
|
47
96
|
version: '0.1.0',
|
|
48
97
|
};
|
|
@@ -50,6 +99,18 @@ const generatePackageJson = (name: string): string => {
|
|
|
50
99
|
return JSON.stringify(pkg, null, 2);
|
|
51
100
|
};
|
|
52
101
|
|
|
102
|
+
const generateScaffoldProvenance = (starter: Starter): string =>
|
|
103
|
+
JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
generatedAt: new Date().toISOString(),
|
|
106
|
+
scaffoldVersion: trailsPackageVersion,
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
template: starter,
|
|
109
|
+
},
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
);
|
|
113
|
+
|
|
53
114
|
const TSCONFIG_CONTENT = JSON.stringify(
|
|
54
115
|
{
|
|
55
116
|
compilerOptions: {
|
|
@@ -70,19 +131,99 @@ const TSCONFIG_CONTENT = JSON.stringify(
|
|
|
70
131
|
2
|
|
71
132
|
);
|
|
72
133
|
|
|
134
|
+
const TSCONFIG_TESTS_CONTENT = JSON.stringify(
|
|
135
|
+
{
|
|
136
|
+
compilerOptions: {
|
|
137
|
+
noEmit: true,
|
|
138
|
+
rootDir: '.',
|
|
139
|
+
types: ['bun'],
|
|
140
|
+
},
|
|
141
|
+
exclude: [],
|
|
142
|
+
extends: './tsconfig.json',
|
|
143
|
+
include: ['src', '__tests__'],
|
|
144
|
+
},
|
|
145
|
+
null,
|
|
146
|
+
2
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const AGENTS_CONTENT = `# AGENTS.md
|
|
150
|
+
|
|
151
|
+
This is a Trails project. Trails is an agent-native, contract-first TypeScript framework: author a trail once with typed input, Result output, examples, intent, and meta; surface it through CLI, MCP, HTTP, or future WebSocket without rewriting the contract.
|
|
152
|
+
|
|
153
|
+
## Commands
|
|
154
|
+
|
|
155
|
+
Use the project scripts first:
|
|
156
|
+
|
|
157
|
+
\`\`\`bash
|
|
158
|
+
bun install
|
|
159
|
+
bun run build
|
|
160
|
+
bun test
|
|
161
|
+
bun run typecheck
|
|
162
|
+
bun run lint
|
|
163
|
+
bun run format:check
|
|
164
|
+
bun run warden
|
|
165
|
+
bun run survey
|
|
166
|
+
bun run guide
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Lexicon
|
|
170
|
+
|
|
171
|
+
- \`trail\`, not action or handler
|
|
172
|
+
- \`blaze\`, not handler or impl
|
|
173
|
+
- \`topo\`, not registry or collection
|
|
174
|
+
- \`compose\`, not follow
|
|
175
|
+
- \`surface\`, not transport
|
|
176
|
+
- \`resource\`, not service or dependency
|
|
177
|
+
- \`layer\`, for compose-cutting trail wrapping
|
|
178
|
+
|
|
179
|
+
## Trail Rules
|
|
180
|
+
|
|
181
|
+
- Blazes return \`Result\`; never throw from trail logic.
|
|
182
|
+
- Use \`Result.ok()\` and \`Result.err()\`; branch with \`isOk()\`, \`isErr()\`, or \`match()\`.
|
|
183
|
+
- Keep trail logic surface-agnostic. Do not import CLI, MCP, HTTP, request, or response types into blazes.
|
|
184
|
+
- Public MCP or HTTP trails declare an \`output\` schema.
|
|
185
|
+
- Trails that compose other trails declare \`composes: [...]\` and invoke them with \`ctx.compose(...)\`.
|
|
186
|
+
- Trails that use infrastructure declare \`resources: [...]\` and access them through the resource helpers.
|
|
187
|
+
- Use \`detours\` for recovery strategies instead of inline retry logic.
|
|
188
|
+
- Prefer examples for happy-path coverage, and add focused tests for edge cases.
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
const CLAUDE_CONTENT = `# CLAUDE.md
|
|
192
|
+
|
|
193
|
+
## Compatibility Shim
|
|
194
|
+
|
|
195
|
+
Keep shared project guidance in \`./AGENTS.md\`. Only Claude-specific bootstrap notes belong here.
|
|
196
|
+
|
|
197
|
+
## Agent Instructions
|
|
198
|
+
|
|
199
|
+
@AGENTS.md
|
|
200
|
+
`;
|
|
201
|
+
|
|
73
202
|
const GITIGNORE_CONTENT = `node_modules/
|
|
74
203
|
dist/
|
|
75
204
|
*.tsbuildinfo
|
|
76
|
-
.trails/
|
|
205
|
+
.trails/cache/
|
|
206
|
+
.trails/state/
|
|
207
|
+
.trails/config.local.js
|
|
208
|
+
.trails/config.local.ts
|
|
77
209
|
`;
|
|
78
210
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
211
|
+
const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
|
|
212
|
+
import ultracite from 'ultracite/oxlint/core';
|
|
213
|
+
|
|
214
|
+
export default defineConfig({
|
|
215
|
+
extends: [ultracite],
|
|
216
|
+
rules: {
|
|
217
|
+
'no-warning-comments': [
|
|
218
|
+
'error',
|
|
219
|
+
{
|
|
220
|
+
location: 'start',
|
|
221
|
+
terms: ['todo:', 'fixme', 'xxx'],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
82
224
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
225
|
+
});
|
|
226
|
+
`;
|
|
86
227
|
|
|
87
228
|
const OXFMTRC_CONTENT = `{
|
|
88
229
|
// ultracite defaults
|
|
@@ -107,7 +248,7 @@ export const hello = trail('hello', {
|
|
|
107
248
|
name: 'Named greeting',
|
|
108
249
|
},
|
|
109
250
|
],
|
|
110
|
-
|
|
251
|
+
blaze: (input) => {
|
|
111
252
|
const name = input.name ?? 'world';
|
|
112
253
|
return Result.ok({ message: \`Hello, \${name}!\` });
|
|
113
254
|
},
|
|
@@ -117,14 +258,18 @@ export const hello = trail('hello', {
|
|
|
117
258
|
output: z.object({
|
|
118
259
|
message: z.string(),
|
|
119
260
|
}),
|
|
120
|
-
|
|
261
|
+
intent: 'read',
|
|
121
262
|
});
|
|
122
263
|
`;
|
|
123
264
|
|
|
124
265
|
const generateEntityTrails = (): string =>
|
|
125
|
-
`import {
|
|
266
|
+
`import { randomUUID } from 'node:crypto';
|
|
267
|
+
|
|
268
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
126
269
|
import { z } from 'zod';
|
|
127
270
|
|
|
271
|
+
import { entityStore } from '../store.js';
|
|
272
|
+
|
|
128
273
|
const entitySchema = z.object({
|
|
129
274
|
id: z.string(),
|
|
130
275
|
name: z.string(),
|
|
@@ -139,28 +284,85 @@ export const show = trail('entity.show', {
|
|
|
139
284
|
name: 'Show entity',
|
|
140
285
|
},
|
|
141
286
|
],
|
|
142
|
-
|
|
143
|
-
|
|
287
|
+
blaze: (input, ctx) => {
|
|
288
|
+
const store = entityStore.from(ctx);
|
|
289
|
+
const entity = store.get(input.id);
|
|
290
|
+
if (!entity) {
|
|
291
|
+
return Result.err(new NotFoundError(\`Entity "\${input.id}" not found\`));
|
|
292
|
+
}
|
|
293
|
+
return Result.ok(entity);
|
|
144
294
|
},
|
|
145
295
|
input: z.object({ id: z.string() }),
|
|
146
296
|
output: entitySchema,
|
|
147
|
-
|
|
297
|
+
intent: 'read',
|
|
298
|
+
resources: [entityStore],
|
|
148
299
|
});
|
|
149
300
|
|
|
150
301
|
export const add = trail('entity.add', {
|
|
151
302
|
description: 'Add a new entity',
|
|
152
303
|
examples: [
|
|
153
304
|
{
|
|
154
|
-
|
|
305
|
+
expectedMatch: { name: 'New' },
|
|
155
306
|
input: { name: 'New' },
|
|
156
307
|
name: 'Add entity',
|
|
157
308
|
},
|
|
158
309
|
],
|
|
159
|
-
|
|
160
|
-
|
|
310
|
+
blaze: (input, ctx) => {
|
|
311
|
+
const store = entityStore.from(ctx);
|
|
312
|
+
const entity = { id: randomUUID(), name: input.name };
|
|
313
|
+
store.add(entity);
|
|
314
|
+
return Result.ok(entity);
|
|
161
315
|
},
|
|
162
316
|
input: z.object({ name: z.string() }),
|
|
163
317
|
output: entitySchema,
|
|
318
|
+
intent: 'write',
|
|
319
|
+
permit: { scopes: ['entity:write'] },
|
|
320
|
+
resources: [entityStore],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
export const list = trail('entity.list', {
|
|
324
|
+
description: 'List entities',
|
|
325
|
+
examples: [
|
|
326
|
+
{
|
|
327
|
+
expected: { entities: [{ id: '1', name: 'Example' }] },
|
|
328
|
+
input: {},
|
|
329
|
+
name: 'List entities',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
blaze: (_input, ctx) => {
|
|
333
|
+
const store = entityStore.from(ctx);
|
|
334
|
+
return Result.ok({ entities: store.list() });
|
|
335
|
+
},
|
|
336
|
+
input: z.object({}),
|
|
337
|
+
output: z.object({
|
|
338
|
+
entities: z.array(entitySchema),
|
|
339
|
+
}),
|
|
340
|
+
intent: 'read',
|
|
341
|
+
resources: [entityStore],
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
export const remove = trail('entity.delete', {
|
|
345
|
+
description: 'Delete an entity by ID',
|
|
346
|
+
examples: [
|
|
347
|
+
{
|
|
348
|
+
expected: { deleted: true, id: '1' },
|
|
349
|
+
input: { id: '1' },
|
|
350
|
+
name: 'Delete entity',
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
blaze: (input, ctx) => {
|
|
354
|
+
const store = entityStore.from(ctx);
|
|
355
|
+
const deleted = store.delete(input.id);
|
|
356
|
+
return Result.ok({ deleted, id: input.id });
|
|
357
|
+
},
|
|
358
|
+
input: z.object({ id: z.string() }),
|
|
359
|
+
output: z.object({
|
|
360
|
+
deleted: z.boolean(),
|
|
361
|
+
id: z.string(),
|
|
362
|
+
}),
|
|
363
|
+
intent: 'destroy',
|
|
364
|
+
permit: { scopes: ['entity:write'] },
|
|
365
|
+
resources: [entityStore],
|
|
164
366
|
});
|
|
165
367
|
`;
|
|
166
368
|
|
|
@@ -177,26 +379,26 @@ export const search = trail('search', {
|
|
|
177
379
|
name: 'Search entities',
|
|
178
380
|
},
|
|
179
381
|
],
|
|
180
|
-
|
|
382
|
+
blaze: () => {
|
|
181
383
|
return Result.ok({ results: [] });
|
|
182
384
|
},
|
|
183
385
|
input: z.object({ query: z.string() }),
|
|
184
386
|
output: z.object({
|
|
185
387
|
results: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
186
388
|
}),
|
|
187
|
-
|
|
389
|
+
intent: 'read',
|
|
188
390
|
});
|
|
189
391
|
`;
|
|
190
392
|
|
|
191
|
-
const
|
|
192
|
-
`import { Result,
|
|
393
|
+
const generateOnboardTrail = (): string =>
|
|
394
|
+
`import { Result, trail } from '@ontrails/core';
|
|
193
395
|
import { z } from 'zod';
|
|
194
396
|
|
|
195
|
-
export const onboard =
|
|
397
|
+
export const onboard = trail('entity.onboard', {
|
|
196
398
|
description: 'Onboard a new entity end-to-end',
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const result = await ctx.
|
|
399
|
+
composes: ['entity.add'],
|
|
400
|
+
blaze: async (input, ctx) => {
|
|
401
|
+
const result = await ctx.compose('entity.add', { name: input.name });
|
|
200
402
|
if (result.isErr()) {
|
|
201
403
|
return result;
|
|
202
404
|
}
|
|
@@ -204,14 +406,16 @@ export const onboard = hike('entity.onboard', {
|
|
|
204
406
|
},
|
|
205
407
|
input: z.object({ name: z.string() }),
|
|
206
408
|
output: z.object({ onboarded: z.boolean() }),
|
|
409
|
+
intent: 'write',
|
|
410
|
+
permit: { scopes: ['entity:write'] },
|
|
207
411
|
});
|
|
208
412
|
`;
|
|
209
413
|
|
|
210
|
-
const
|
|
211
|
-
`import {
|
|
414
|
+
const generateEntitySignals = (): string =>
|
|
415
|
+
`import { signal } from '@ontrails/core';
|
|
212
416
|
import { z } from 'zod';
|
|
213
417
|
|
|
214
|
-
export const entityUpdated =
|
|
418
|
+
export const entityUpdated = signal('entity.updated', {
|
|
215
419
|
description: 'Fired when an entity is updated',
|
|
216
420
|
payload: z.object({
|
|
217
421
|
entityId: z.string(),
|
|
@@ -221,21 +425,49 @@ export const entityUpdated = event('entity.updated', {
|
|
|
221
425
|
`;
|
|
222
426
|
|
|
223
427
|
const generateStore = (): string =>
|
|
224
|
-
|
|
428
|
+
`import { Result, resource } from '@ontrails/core';
|
|
225
429
|
|
|
226
|
-
|
|
430
|
+
/** In-memory store for entities. */
|
|
431
|
+
|
|
432
|
+
export interface Entity {
|
|
227
433
|
readonly id: string;
|
|
228
434
|
readonly name: string;
|
|
229
435
|
}
|
|
230
436
|
|
|
231
|
-
|
|
437
|
+
export interface EntityStore {
|
|
438
|
+
add(entity: Entity): void;
|
|
439
|
+
delete(id: string): boolean;
|
|
440
|
+
get(id: string): Entity | undefined;
|
|
441
|
+
list(): Entity[];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const defaultEntities: readonly Entity[] = [{ id: '1', name: 'Example' }];
|
|
232
445
|
|
|
233
|
-
export const
|
|
234
|
-
|
|
235
|
-
|
|
446
|
+
export const createEntityStore = (
|
|
447
|
+
seed: readonly Entity[] = defaultEntities
|
|
448
|
+
): EntityStore => {
|
|
449
|
+
const store = new Map(seed.map((entity) => [entity.id, entity] as const));
|
|
450
|
+
return {
|
|
451
|
+
add(entity) {
|
|
452
|
+
store.set(entity.id, entity);
|
|
453
|
+
},
|
|
454
|
+
delete(id) {
|
|
455
|
+
return store.delete(id);
|
|
456
|
+
},
|
|
457
|
+
get(id) {
|
|
458
|
+
return store.get(id);
|
|
459
|
+
},
|
|
460
|
+
list() {
|
|
461
|
+
return Array.from(store.values());
|
|
462
|
+
},
|
|
463
|
+
};
|
|
236
464
|
};
|
|
237
|
-
|
|
238
|
-
export const
|
|
465
|
+
|
|
466
|
+
export const entityStore = resource('entity.store', {
|
|
467
|
+
description: 'In-memory entity store for the entity starter.',
|
|
468
|
+
create: () => Result.ok(createEntityStore()),
|
|
469
|
+
mock: createEntityStore,
|
|
470
|
+
});
|
|
239
471
|
`;
|
|
240
472
|
|
|
241
473
|
const starterImports: Record<
|
|
@@ -248,9 +480,10 @@ const starterImports: Record<
|
|
|
248
480
|
"import * as entity from './trails/entity.js';",
|
|
249
481
|
"import * as search from './trails/search.js';",
|
|
250
482
|
"import * as onboard from './trails/onboard.js';",
|
|
251
|
-
"import * as
|
|
483
|
+
"import * as entitySignals from './signals/entity-signals.js';",
|
|
484
|
+
"import * as store from './store.js';",
|
|
252
485
|
],
|
|
253
|
-
modules: ['entity', 'search', 'onboard', '
|
|
486
|
+
modules: ['entity', 'search', 'onboard', 'entitySignals', 'store'],
|
|
254
487
|
},
|
|
255
488
|
hello: {
|
|
256
489
|
imports: ["import * as hello from './trails/hello.js';"],
|
|
@@ -260,8 +493,11 @@ const starterImports: Record<
|
|
|
260
493
|
|
|
261
494
|
const generateAppTs = (name: string, starter: Starter): string => {
|
|
262
495
|
const { imports, modules } = starterImports[starter];
|
|
496
|
+
const appNameLiteral = JSON.stringify(name);
|
|
263
497
|
const topoArgs =
|
|
264
|
-
modules.length > 0
|
|
498
|
+
modules.length > 0
|
|
499
|
+
? `${appNameLiteral}, ${modules.join(', ')}`
|
|
500
|
+
: appNameLiteral;
|
|
265
501
|
|
|
266
502
|
return [
|
|
267
503
|
"import { topo } from '@ontrails/core';",
|
|
@@ -281,8 +517,8 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
|
|
|
281
517
|
entity: () => [
|
|
282
518
|
['src/trails/entity.ts', generateEntityTrails()],
|
|
283
519
|
['src/trails/search.ts', generateSearchTrail()],
|
|
284
|
-
['src/trails/onboard.ts',
|
|
285
|
-
['src/
|
|
520
|
+
['src/trails/onboard.ts', generateOnboardTrail()],
|
|
521
|
+
['src/signals/entity-signals.ts', generateEntitySignals()],
|
|
286
522
|
['src/store.ts', generateStore()],
|
|
287
523
|
],
|
|
288
524
|
hello: () => [['src/trails/hello.ts', generateHelloTrail()]],
|
|
@@ -294,59 +530,103 @@ const collectScaffoldFiles = (
|
|
|
294
530
|
): Map<string, string> =>
|
|
295
531
|
new Map([
|
|
296
532
|
['package.json', generatePackageJson(name)],
|
|
533
|
+
['AGENTS.md', AGENTS_CONTENT],
|
|
534
|
+
['CLAUDE.md', CLAUDE_CONTENT],
|
|
297
535
|
['tsconfig.json', TSCONFIG_CONTENT],
|
|
536
|
+
['tsconfig.tests.json', TSCONFIG_TESTS_CONTENT],
|
|
298
537
|
['.gitignore', GITIGNORE_CONTENT],
|
|
299
|
-
['.
|
|
538
|
+
['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
|
|
300
539
|
['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
|
|
540
|
+
['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
|
|
541
|
+
['.trails/scaffold.json', generateScaffoldProvenance(starter)],
|
|
301
542
|
['src/app.ts', generateAppTs(name, starter)],
|
|
302
543
|
...starterFileGenerators[starter](),
|
|
303
544
|
]);
|
|
304
545
|
|
|
305
|
-
const
|
|
306
|
-
projectDir: string,
|
|
546
|
+
const collectScaffoldOperations = (
|
|
307
547
|
fileMap: Map<string, string>
|
|
308
|
-
):
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
files.push(relativePath);
|
|
315
|
-
}
|
|
316
|
-
return files;
|
|
317
|
-
};
|
|
548
|
+
): ProjectWriteOperation[] =>
|
|
549
|
+
[...fileMap].map(([path, content]) => ({
|
|
550
|
+
content,
|
|
551
|
+
kind: 'write' as const,
|
|
552
|
+
path,
|
|
553
|
+
}));
|
|
318
554
|
|
|
319
555
|
// ---------------------------------------------------------------------------
|
|
320
556
|
// Trail definition
|
|
321
557
|
// ---------------------------------------------------------------------------
|
|
322
558
|
|
|
323
559
|
export const createScaffold = trail('create.scaffold', {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
560
|
+
blaze: async (input) => {
|
|
561
|
+
const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
|
|
562
|
+
if (projectDirResult.isErr()) {
|
|
563
|
+
return projectDirResult;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const projectDir = projectDirResult.value;
|
|
327
567
|
const starter = (input.starter ?? 'hello') as Starter;
|
|
568
|
+
const dryRun = input.dryRun === true;
|
|
328
569
|
const fileMap = collectScaffoldFiles(input.name, starter);
|
|
329
|
-
const
|
|
330
|
-
|
|
570
|
+
const operations = collectScaffoldOperations(fileMap);
|
|
571
|
+
const plannedOperations = dryRun
|
|
572
|
+
? planProjectOperations(projectDir, operations, { existing: 'preserve' })
|
|
573
|
+
: await applyProjectOperations(projectDir, operations, {
|
|
574
|
+
existing: 'preserve',
|
|
575
|
+
});
|
|
576
|
+
if (plannedOperations.isErr()) {
|
|
577
|
+
return Result.err(plannedOperations.error);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const created = dryRun
|
|
581
|
+
? []
|
|
582
|
+
: plannedOperations.value
|
|
583
|
+
.filter((operation) => operation.kind === 'write')
|
|
584
|
+
.map((operation) => operation.path);
|
|
331
585
|
|
|
332
586
|
return Result.ok({
|
|
333
|
-
created
|
|
334
|
-
dir: projectDir,
|
|
587
|
+
created,
|
|
588
|
+
dir: resolve(projectDir),
|
|
589
|
+
dryRun,
|
|
335
590
|
name: input.name,
|
|
591
|
+
plannedOperations: plannedOperations.value,
|
|
336
592
|
} satisfies ScaffoldResult);
|
|
337
593
|
},
|
|
594
|
+
description: 'Scaffold a new Trails project',
|
|
338
595
|
input: z.object({
|
|
339
596
|
dir: z.string().optional().describe('Parent directory'),
|
|
340
|
-
|
|
597
|
+
dryRun: z
|
|
598
|
+
.boolean()
|
|
599
|
+
.default(false)
|
|
600
|
+
.describe('Plan scaffold writes without touching the project directory'),
|
|
601
|
+
name: z
|
|
602
|
+
.string()
|
|
603
|
+
.regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
|
|
604
|
+
.describe('Project name'),
|
|
341
605
|
starter: z
|
|
342
606
|
.enum(['hello', 'entity', 'empty'])
|
|
343
607
|
.default('hello')
|
|
344
608
|
.describe('Starter trail'),
|
|
345
609
|
}),
|
|
346
|
-
|
|
610
|
+
intent: 'write',
|
|
347
611
|
output: z.object({
|
|
348
|
-
created: z
|
|
612
|
+
created: z
|
|
613
|
+
.array(z.string())
|
|
614
|
+
.describe('Project-relative paths of files written (empty in dry-run)'),
|
|
349
615
|
dir: z.string(),
|
|
616
|
+
dryRun: z.boolean(),
|
|
350
617
|
name: z.string(),
|
|
618
|
+
plannedOperations: z.array(
|
|
619
|
+
z.discriminatedUnion('kind', [
|
|
620
|
+
z.object({ kind: z.literal('mkdir'), path: z.string() }),
|
|
621
|
+
z.object({
|
|
622
|
+
from: z.string(),
|
|
623
|
+
kind: z.literal('rename'),
|
|
624
|
+
to: z.string(),
|
|
625
|
+
}),
|
|
626
|
+
z.object({ kind: z.literal('write'), path: z.string() }),
|
|
627
|
+
])
|
|
628
|
+
),
|
|
351
629
|
}),
|
|
630
|
+
permit: { scopes: ['project:write'] },
|
|
631
|
+
visibility: 'internal',
|
|
352
632
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `create.versions` trail -- Sync generated scaffold dependency versions.
|
|
3
|
+
*
|
|
4
|
+
* Derives `apps/trails/src/scaffold-versions.generated.ts` from the root
|
|
5
|
+
* `package.json` catalog and devDependencies. Graduated from
|
|
6
|
+
* `scripts/sync-scaffold-versions.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Result, trail, ValidationError } from '@ontrails/core';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
import { syncScaffoldVersions } from '../scaffold-version-sync.js';
|
|
13
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
14
|
+
|
|
15
|
+
const createVersionsInputSchema = z.object({
|
|
16
|
+
check: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe('Verify the generated file is current instead of writing'),
|
|
20
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const createVersionsOutputSchema = z.object({
|
|
24
|
+
generatedPath: z.string(),
|
|
25
|
+
mode: z.enum(['check', 'write']),
|
|
26
|
+
written: z.boolean(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const createVersionsTrail = trail('create.versions', {
|
|
30
|
+
blaze: async (input, ctx) => {
|
|
31
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
32
|
+
if (rootDirResult.isErr()) {
|
|
33
|
+
return rootDirResult;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return Result.ok(
|
|
38
|
+
await syncScaffoldVersions({
|
|
39
|
+
check: input.check,
|
|
40
|
+
rootDir: rootDirResult.value,
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return Result.err(
|
|
45
|
+
new ValidationError(
|
|
46
|
+
error instanceof Error ? error.message : String(error)
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
description: 'Sync generated scaffold dependency versions',
|
|
52
|
+
examples: [
|
|
53
|
+
{
|
|
54
|
+
input: { check: true },
|
|
55
|
+
name: 'Verify generated scaffold versions are current',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
input: createVersionsInputSchema,
|
|
59
|
+
intent: 'write',
|
|
60
|
+
output: createVersionsOutputSchema,
|
|
61
|
+
permit: { scopes: ['project:write'] },
|
|
62
|
+
});
|