@ontrails/trails 1.0.0-beta.5 → 1.0.0-beta.50
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 +1213 -4
- package/README.md +29 -0
- package/package.json +35 -7
- package/src/app.ts +147 -3
- package/src/cli.ts +358 -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 +42 -0
- package/src/mcp-options.ts +92 -0
- package/src/mcp.ts +8 -0
- package/src/project-writes.ts +377 -0
- package/src/regrade/audit.ts +571 -0
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +636 -0
- package/src/regrade/lifecycle.ts +76 -0
- package/src/regrade/live-api-preserve.ts +123 -0
- package/src/regrade/plan-artifact.ts +461 -0
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/prepared-run.ts +259 -0
- package/src/regrade/receipt-history.ts +446 -0
- package/src/regrade/source-transaction.ts +185 -0
- package/src/release/bindings.ts +58 -0
- package/src/release/check.ts +1146 -0
- package/src/release/cli-bundle.ts +575 -0
- package/src/release/config.ts +73 -0
- package/src/release/contract-facts.ts +425 -0
- package/src/release/homebrew.ts +219 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +230 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +830 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +455 -0
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/packed-artifacts-smoke.ts +236 -0
- package/src/release/policy.ts +1700 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -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-regrade-progress.ts +47 -0
- package/src/run-release-check.ts +74 -0
- package/src/run-schema.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/run-wayfind-outline.ts +170 -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 +99 -45
- package/src/trails/add-trail.ts +84 -37
- package/src/trails/add-verify.ts +100 -30
- package/src/trails/compile.ts +58 -0
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +402 -106
- 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 +70 -68
- package/src/trails/load-app.ts +1123 -15
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project.ts +17 -3
- package/src/trails/regrade.ts +4497 -0
- package/src/trails/release-check.ts +113 -0
- package/src/trails/release-smoke.ts +49 -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 +877 -214
- package/src/trails/topo-activation.ts +14 -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 +368 -0
- package/src/trails/topo-reports.ts +809 -0
- package/src/trails/topo-store-support.ts +323 -0
- package/src/trails/topo-support.ts +228 -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 +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +198 -58
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1053 -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 -10
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -77
- 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 -31
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -221
- 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 -159
- 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,195 @@ 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
|
|
127
|
+
},
|
|
128
|
+
"include": ["src"]
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
const TSCONFIG_TESTS_CONTENT = `{
|
|
133
|
+
"compilerOptions": {
|
|
134
|
+
"noEmit": true,
|
|
135
|
+
"rootDir": ".",
|
|
136
|
+
"types": ["bun"]
|
|
68
137
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
- \`implementation\`, 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
|
+
- Implementations 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 implementations.
|
|
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,
|
|
231
|
+
"ignorePatterns": ["**/.trails/regrade/history/**"],
|
|
89
232
|
}
|
|
90
233
|
`;
|
|
91
234
|
|
|
@@ -94,6 +237,10 @@ const generateHelloTrail = (): string =>
|
|
|
94
237
|
import { z } from 'zod';
|
|
95
238
|
|
|
96
239
|
export const hello = trail('hello', {
|
|
240
|
+
implementation: (input) => {
|
|
241
|
+
const name = input.name ?? 'world';
|
|
242
|
+
return Result.ok({ message: \`Hello, \${name}!\` });
|
|
243
|
+
},
|
|
97
244
|
description: 'Say hello',
|
|
98
245
|
examples: [
|
|
99
246
|
{
|
|
@@ -107,30 +254,38 @@ export const hello = trail('hello', {
|
|
|
107
254
|
name: 'Named greeting',
|
|
108
255
|
},
|
|
109
256
|
],
|
|
110
|
-
run: (input) => {
|
|
111
|
-
const name = input.name ?? 'world';
|
|
112
|
-
return Result.ok({ message: \`Hello, \${name}!\` });
|
|
113
|
-
},
|
|
114
257
|
input: z.object({
|
|
115
258
|
name: z.string().optional(),
|
|
116
259
|
}),
|
|
260
|
+
intent: 'read',
|
|
117
261
|
output: z.object({
|
|
118
262
|
message: z.string(),
|
|
119
263
|
}),
|
|
120
|
-
intent: 'read',
|
|
121
264
|
});
|
|
122
265
|
`;
|
|
123
266
|
|
|
124
267
|
const generateEntityTrails = (): string =>
|
|
125
|
-
`import {
|
|
268
|
+
`import { randomUUID } from 'node:crypto';
|
|
269
|
+
|
|
270
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
126
271
|
import { z } from 'zod';
|
|
127
272
|
|
|
273
|
+
import { entityStore } from '../store.js';
|
|
274
|
+
|
|
128
275
|
const entitySchema = z.object({
|
|
129
276
|
id: z.string(),
|
|
130
277
|
name: z.string(),
|
|
131
278
|
});
|
|
132
279
|
|
|
133
280
|
export const show = trail('entity.show', {
|
|
281
|
+
implementation: (input, ctx) => {
|
|
282
|
+
const store = entityStore.from(ctx);
|
|
283
|
+
const entity = store.get(input.id);
|
|
284
|
+
if (!entity) {
|
|
285
|
+
return Result.err(new NotFoundError(\`Entity "\${input.id}" not found\`));
|
|
286
|
+
}
|
|
287
|
+
return Result.ok(entity);
|
|
288
|
+
},
|
|
134
289
|
description: 'Show an entity by ID',
|
|
135
290
|
examples: [
|
|
136
291
|
{
|
|
@@ -139,28 +294,77 @@ export const show = trail('entity.show', {
|
|
|
139
294
|
name: 'Show entity',
|
|
140
295
|
},
|
|
141
296
|
],
|
|
142
|
-
run: (input) => {
|
|
143
|
-
return Result.ok({ id: input.id, name: 'Example' });
|
|
144
|
-
},
|
|
145
297
|
input: z.object({ id: z.string() }),
|
|
146
|
-
output: entitySchema,
|
|
147
298
|
intent: 'read',
|
|
299
|
+
output: entitySchema,
|
|
300
|
+
resources: [entityStore],
|
|
148
301
|
});
|
|
149
302
|
|
|
150
303
|
export const add = trail('entity.add', {
|
|
304
|
+
implementation: (input, ctx) => {
|
|
305
|
+
const store = entityStore.from(ctx);
|
|
306
|
+
const entity = { id: randomUUID(), name: input.name };
|
|
307
|
+
store.add(entity);
|
|
308
|
+
return Result.ok(entity);
|
|
309
|
+
},
|
|
151
310
|
description: 'Add a new entity',
|
|
152
311
|
examples: [
|
|
153
312
|
{
|
|
154
|
-
|
|
313
|
+
expectedMatch: { name: 'New' },
|
|
155
314
|
input: { name: 'New' },
|
|
156
315
|
name: 'Add entity',
|
|
157
316
|
},
|
|
158
317
|
],
|
|
159
|
-
run: (input) => {
|
|
160
|
-
return Result.ok({ id: '1', name: input.name });
|
|
161
|
-
},
|
|
162
318
|
input: z.object({ name: z.string() }),
|
|
319
|
+
intent: 'write',
|
|
163
320
|
output: entitySchema,
|
|
321
|
+
permit: { scopes: ['entity:write'] },
|
|
322
|
+
resources: [entityStore],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
export const list = trail('entity.list', {
|
|
326
|
+
implementation: (_input, ctx) => {
|
|
327
|
+
const store = entityStore.from(ctx);
|
|
328
|
+
return Result.ok({ entities: store.list() });
|
|
329
|
+
},
|
|
330
|
+
description: 'List entities',
|
|
331
|
+
examples: [
|
|
332
|
+
{
|
|
333
|
+
expected: { entities: [{ id: '1', name: 'Example' }] },
|
|
334
|
+
input: {},
|
|
335
|
+
name: 'List entities',
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
input: z.object({}),
|
|
339
|
+
intent: 'read',
|
|
340
|
+
output: z.object({
|
|
341
|
+
entities: z.array(entitySchema),
|
|
342
|
+
}),
|
|
343
|
+
resources: [entityStore],
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
export const remove = trail('entity.delete', {
|
|
347
|
+
implementation: (input, ctx) => {
|
|
348
|
+
const store = entityStore.from(ctx);
|
|
349
|
+
const deleted = store.delete(input.id);
|
|
350
|
+
return Result.ok({ deleted, id: input.id });
|
|
351
|
+
},
|
|
352
|
+
description: 'Delete an entity by ID',
|
|
353
|
+
examples: [
|
|
354
|
+
{
|
|
355
|
+
expected: { deleted: true, id: '1' },
|
|
356
|
+
input: { id: '1' },
|
|
357
|
+
name: 'Delete entity',
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
input: z.object({ id: z.string() }),
|
|
361
|
+
intent: 'destroy',
|
|
362
|
+
output: z.object({
|
|
363
|
+
deleted: z.boolean(),
|
|
364
|
+
id: z.string(),
|
|
365
|
+
}),
|
|
366
|
+
permit: { scopes: ['entity:write'] },
|
|
367
|
+
resources: [entityStore],
|
|
164
368
|
});
|
|
165
369
|
`;
|
|
166
370
|
|
|
@@ -169,6 +373,9 @@ const generateSearchTrail = (): string =>
|
|
|
169
373
|
import { z } from 'zod';
|
|
170
374
|
|
|
171
375
|
export const search = trail('search', {
|
|
376
|
+
implementation: () => {
|
|
377
|
+
return Result.ok({ results: [] });
|
|
378
|
+
},
|
|
172
379
|
description: 'Search entities by query',
|
|
173
380
|
examples: [
|
|
174
381
|
{
|
|
@@ -177,14 +384,11 @@ export const search = trail('search', {
|
|
|
177
384
|
name: 'Search entities',
|
|
178
385
|
},
|
|
179
386
|
],
|
|
180
|
-
run: () => {
|
|
181
|
-
return Result.ok({ results: [] });
|
|
182
|
-
},
|
|
183
387
|
input: z.object({ query: z.string() }),
|
|
388
|
+
intent: 'read',
|
|
184
389
|
output: z.object({
|
|
185
390
|
results: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
186
391
|
}),
|
|
187
|
-
intent: 'read',
|
|
188
392
|
});
|
|
189
393
|
`;
|
|
190
394
|
|
|
@@ -193,25 +397,27 @@ const generateOnboardTrail = (): string =>
|
|
|
193
397
|
import { z } from 'zod';
|
|
194
398
|
|
|
195
399
|
export const onboard = trail('entity.onboard', {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
run: async (input, ctx) => {
|
|
199
|
-
const result = await ctx.follow('entity.add', { name: input.name });
|
|
400
|
+
implementation: 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
|
}
|
|
203
405
|
return Result.ok({ onboarded: true });
|
|
204
406
|
},
|
|
407
|
+
composes: ['entity.add'],
|
|
408
|
+
description: 'Onboard a new entity end-to-end',
|
|
205
409
|
input: z.object({ name: z.string() }),
|
|
410
|
+
intent: 'write',
|
|
206
411
|
output: z.object({ onboarded: z.boolean() }),
|
|
412
|
+
permit: { scopes: ['entity:write'] },
|
|
207
413
|
});
|
|
208
414
|
`;
|
|
209
415
|
|
|
210
|
-
const
|
|
211
|
-
`import {
|
|
416
|
+
const generateEntitySignals = (): string =>
|
|
417
|
+
`import { signal } from '@ontrails/core';
|
|
212
418
|
import { z } from 'zod';
|
|
213
419
|
|
|
214
|
-
export const entityUpdated =
|
|
420
|
+
export const entityUpdated = signal('entity.updated', {
|
|
215
421
|
description: 'Fired when an entity is updated',
|
|
216
422
|
payload: z.object({
|
|
217
423
|
entityId: z.string(),
|
|
@@ -221,21 +427,49 @@ export const entityUpdated = event('entity.updated', {
|
|
|
221
427
|
`;
|
|
222
428
|
|
|
223
429
|
const generateStore = (): string =>
|
|
224
|
-
|
|
430
|
+
`import { Result, resource } from '@ontrails/core';
|
|
431
|
+
|
|
432
|
+
/** In-memory store for entities. */
|
|
225
433
|
|
|
226
|
-
interface Entity {
|
|
434
|
+
export interface Entity {
|
|
227
435
|
readonly id: string;
|
|
228
436
|
readonly name: string;
|
|
229
437
|
}
|
|
230
438
|
|
|
231
|
-
|
|
439
|
+
export interface EntityStore {
|
|
440
|
+
add(entity: Entity): void;
|
|
441
|
+
delete(id: string): boolean;
|
|
442
|
+
get(id: string): Entity | undefined;
|
|
443
|
+
list(): Entity[];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const defaultEntities: readonly Entity[] = [{ id: '1', name: 'Example' }];
|
|
232
447
|
|
|
233
|
-
export const
|
|
234
|
-
|
|
235
|
-
|
|
448
|
+
export const createEntityStore = (
|
|
449
|
+
seed: readonly Entity[] = defaultEntities
|
|
450
|
+
): EntityStore => {
|
|
451
|
+
const store = new Map(seed.map((entity) => [entity.id, entity] as const));
|
|
452
|
+
return {
|
|
453
|
+
add(entity) {
|
|
454
|
+
store.set(entity.id, entity);
|
|
455
|
+
},
|
|
456
|
+
delete(id) {
|
|
457
|
+
return store.delete(id);
|
|
458
|
+
},
|
|
459
|
+
get(id) {
|
|
460
|
+
return store.get(id);
|
|
461
|
+
},
|
|
462
|
+
list() {
|
|
463
|
+
return [...store.values()];
|
|
464
|
+
},
|
|
465
|
+
};
|
|
236
466
|
};
|
|
237
|
-
|
|
238
|
-
export const
|
|
467
|
+
|
|
468
|
+
export const entityStore = resource('entity.store', {
|
|
469
|
+
create: () => Result.ok(createEntityStore()),
|
|
470
|
+
description: 'In-memory entity store for the entity starter.',
|
|
471
|
+
mock: createEntityStore,
|
|
472
|
+
});
|
|
239
473
|
`;
|
|
240
474
|
|
|
241
475
|
const starterImports: Record<
|
|
@@ -248,9 +482,10 @@ const starterImports: Record<
|
|
|
248
482
|
"import * as entity from './trails/entity.js';",
|
|
249
483
|
"import * as search from './trails/search.js';",
|
|
250
484
|
"import * as onboard from './trails/onboard.js';",
|
|
251
|
-
"import * as
|
|
485
|
+
"import * as entitySignals from './signals/entity-signals.js';",
|
|
486
|
+
"import * as store from './store.js';",
|
|
252
487
|
],
|
|
253
|
-
modules: ['entity', 'search', 'onboard', '
|
|
488
|
+
modules: ['entity', 'search', 'onboard', 'entitySignals', 'store'],
|
|
254
489
|
},
|
|
255
490
|
hello: {
|
|
256
491
|
imports: ["import * as hello from './trails/hello.js';"],
|
|
@@ -258,16 +493,31 @@ const starterImports: Record<
|
|
|
258
493
|
},
|
|
259
494
|
};
|
|
260
495
|
|
|
496
|
+
const renderTopoExpression = (
|
|
497
|
+
appNameLiteral: string,
|
|
498
|
+
modules: readonly string[]
|
|
499
|
+
): string => {
|
|
500
|
+
if (modules.length === 0) {
|
|
501
|
+
return `topo(${appNameLiteral})`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (modules.length === 1) {
|
|
505
|
+
return `topo(${appNameLiteral}, ${modules[0]})`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return `topo(\n ${[appNameLiteral, ...modules].join(',\n ')}\n)`;
|
|
509
|
+
};
|
|
510
|
+
|
|
261
511
|
const generateAppTs = (name: string, starter: Starter): string => {
|
|
262
512
|
const { imports, modules } = starterImports[starter];
|
|
263
|
-
const
|
|
264
|
-
|
|
513
|
+
const appNameLiteral = `'${name}'`;
|
|
514
|
+
const topoExpression = renderTopoExpression(appNameLiteral, modules);
|
|
265
515
|
|
|
266
516
|
return [
|
|
267
517
|
"import { topo } from '@ontrails/core';",
|
|
268
518
|
...imports,
|
|
269
519
|
'',
|
|
270
|
-
`export const app =
|
|
520
|
+
`export const app = ${topoExpression};`,
|
|
271
521
|
'',
|
|
272
522
|
].join('\n');
|
|
273
523
|
};
|
|
@@ -282,7 +532,7 @@ const starterFileGenerators: Record<Starter, () => [string, string][]> = {
|
|
|
282
532
|
['src/trails/entity.ts', generateEntityTrails()],
|
|
283
533
|
['src/trails/search.ts', generateSearchTrail()],
|
|
284
534
|
['src/trails/onboard.ts', generateOnboardTrail()],
|
|
285
|
-
['src/
|
|
535
|
+
['src/signals/entity-signals.ts', generateEntitySignals()],
|
|
286
536
|
['src/store.ts', generateStore()],
|
|
287
537
|
],
|
|
288
538
|
hello: () => [['src/trails/hello.ts', generateHelloTrail()]],
|
|
@@ -294,27 +544,26 @@ const collectScaffoldFiles = (
|
|
|
294
544
|
): Map<string, string> =>
|
|
295
545
|
new Map([
|
|
296
546
|
['package.json', generatePackageJson(name)],
|
|
547
|
+
['AGENTS.md', AGENTS_CONTENT],
|
|
548
|
+
['CLAUDE.md', CLAUDE_CONTENT],
|
|
297
549
|
['tsconfig.json', TSCONFIG_CONTENT],
|
|
550
|
+
['tsconfig.tests.json', TSCONFIG_TESTS_CONTENT],
|
|
298
551
|
['.gitignore', GITIGNORE_CONTENT],
|
|
299
|
-
['.
|
|
552
|
+
['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
|
|
300
553
|
['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
|
|
554
|
+
['.trails/scaffold.json', generateScaffoldProvenance(starter)],
|
|
301
555
|
['src/app.ts', generateAppTs(name, starter)],
|
|
302
556
|
...starterFileGenerators[starter](),
|
|
303
557
|
]);
|
|
304
558
|
|
|
305
|
-
const
|
|
306
|
-
projectDir: string,
|
|
559
|
+
const collectScaffoldOperations = (
|
|
307
560
|
fileMap: Map<string, string>
|
|
308
|
-
):
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
files.push(relativePath);
|
|
315
|
-
}
|
|
316
|
-
return files;
|
|
317
|
-
};
|
|
561
|
+
): ProjectWriteOperation[] =>
|
|
562
|
+
[...fileMap].map(([path, content]) => ({
|
|
563
|
+
content,
|
|
564
|
+
kind: 'write' as const,
|
|
565
|
+
path,
|
|
566
|
+
}));
|
|
318
567
|
|
|
319
568
|
// ---------------------------------------------------------------------------
|
|
320
569
|
// Trail definition
|
|
@@ -322,31 +571,78 @@ const writeScaffoldFiles = async (
|
|
|
322
571
|
|
|
323
572
|
export const createScaffold = trail('create.scaffold', {
|
|
324
573
|
description: 'Scaffold a new Trails project',
|
|
574
|
+
implementation: async (input) => {
|
|
575
|
+
const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
|
|
576
|
+
if (projectDirResult.isErr()) {
|
|
577
|
+
return projectDirResult;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const projectDir = projectDirResult.value;
|
|
581
|
+
const starter = (input.starter ?? 'hello') as Starter;
|
|
582
|
+
const dryRun = input.dryRun === true;
|
|
583
|
+
const fileMap = collectScaffoldFiles(input.name, starter);
|
|
584
|
+
const operations = collectScaffoldOperations(fileMap);
|
|
585
|
+
const plannedOperations: TrailsResult<PlannedProjectOperation[], Error> =
|
|
586
|
+
dryRun
|
|
587
|
+
? planProjectOperations(projectDir, operations, {
|
|
588
|
+
existing: 'preserve',
|
|
589
|
+
})
|
|
590
|
+
: await applyProjectOperations(projectDir, operations, {
|
|
591
|
+
existing: 'preserve',
|
|
592
|
+
});
|
|
593
|
+
if (plannedOperations.isErr()) {
|
|
594
|
+
return plannedOperations;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const created = dryRun
|
|
598
|
+
? []
|
|
599
|
+
: plannedOperations.value
|
|
600
|
+
.filter((operation) => operation.kind === 'write')
|
|
601
|
+
.map((operation) => operation.path);
|
|
602
|
+
|
|
603
|
+
return Result.ok({
|
|
604
|
+
created,
|
|
605
|
+
dir: resolve(projectDir),
|
|
606
|
+
dryRun,
|
|
607
|
+
name: input.name,
|
|
608
|
+
plannedOperations: plannedOperations.value,
|
|
609
|
+
} satisfies ScaffoldResult);
|
|
610
|
+
},
|
|
325
611
|
input: z.object({
|
|
326
612
|
dir: z.string().optional().describe('Parent directory'),
|
|
327
|
-
|
|
613
|
+
dryRun: z
|
|
614
|
+
.boolean()
|
|
615
|
+
.default(false)
|
|
616
|
+
.describe('Plan scaffold writes without touching the project directory'),
|
|
617
|
+
name: z
|
|
618
|
+
.string()
|
|
619
|
+
.regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
|
|
620
|
+
.describe('Project name'),
|
|
328
621
|
starter: z
|
|
329
622
|
.enum(['hello', 'entity', 'empty'])
|
|
330
623
|
.default('hello')
|
|
331
624
|
.describe('Starter trail'),
|
|
332
625
|
}),
|
|
333
|
-
|
|
626
|
+
intent: 'write',
|
|
334
627
|
output: z.object({
|
|
335
|
-
created: z
|
|
628
|
+
created: z
|
|
629
|
+
.array(z.string())
|
|
630
|
+
.describe('Project-relative paths of files written (empty in dry-run)'),
|
|
336
631
|
dir: z.string(),
|
|
632
|
+
dryRun: z.boolean(),
|
|
337
633
|
name: z.string(),
|
|
634
|
+
plannedOperations: z.array(
|
|
635
|
+
z.discriminatedUnion('kind', [
|
|
636
|
+
z.object({ kind: z.literal('mkdir'), path: z.string() }),
|
|
637
|
+
z.object({
|
|
638
|
+
from: z.string(),
|
|
639
|
+
kind: z.literal('rename'),
|
|
640
|
+
to: z.string(),
|
|
641
|
+
}),
|
|
642
|
+
z.object({ kind: z.literal('write'), path: z.string() }),
|
|
643
|
+
])
|
|
644
|
+
),
|
|
338
645
|
}),
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const starter = (input.starter ?? 'hello') as Starter;
|
|
342
|
-
const fileMap = collectScaffoldFiles(input.name, starter);
|
|
343
|
-
const files = await writeScaffoldFiles(projectDir, fileMap);
|
|
344
|
-
mkdirSync(join(projectDir, '.trails'), { recursive: true });
|
|
345
|
-
|
|
346
|
-
return Result.ok({
|
|
347
|
-
created: files,
|
|
348
|
-
dir: projectDir,
|
|
349
|
-
name: input.name,
|
|
350
|
-
} satisfies ScaffoldResult);
|
|
351
|
-
},
|
|
646
|
+
permit: { scopes: ['project:write'] },
|
|
647
|
+
visibility: 'internal',
|
|
352
648
|
});
|