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