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