@ontrails/trails 1.0.0-beta.18 → 1.0.0-beta.19
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 +117 -0
- package/README.md +7 -10
- package/package.json +13 -12
- package/src/app.ts +14 -4
- package/src/cli.ts +16 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/project-writes.ts +62 -5
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-collision.ts +1 -0
- package/src/trails/adapter-check.ts +244 -0
- package/src/trails/add-surface.ts +18 -18
- package/src/trails/add-trail.ts +3 -2
- package/src/trails/add-verify.ts +30 -6
- package/src/trails/{topo-compile.ts → compile.ts} +16 -8
- package/src/trails/completions-complete.ts +1 -1
- package/src/trails/create-adapter.ts +1084 -0
- package/src/trails/create-scaffold.ts +243 -29
- package/src/trails/create.ts +118 -17
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +2 -2
- package/src/trails/dev-reset.ts +2 -2
- package/src/trails/dev-stats.ts +1 -1
- package/src/trails/doctor.ts +56 -0
- package/src/trails/draft-promote.ts +1 -0
- package/src/trails/guide.ts +2 -2
- package/src/trails/revise.ts +53 -0
- package/src/trails/run-example.ts +12 -7
- package/src/trails/run-examples.ts +3 -3
- package/src/trails/run.ts +7 -4
- package/src/trails/survey.ts +332 -25
- package/src/trails/topo-history.ts +1 -1
- package/src/trails/topo-output-schemas.ts +30 -1
- package/src/trails/topo-pin.ts +3 -2
- package/src/trails/topo-read-support.ts +49 -8
- package/src/trails/topo-reports.ts +39 -22
- package/src/trails/topo-store-support.ts +62 -16
- package/src/trails/topo-support.ts +1 -1
- package/src/trails/topo-unpin.ts +2 -2
- package/src/trails/topo.ts +2 -2
- package/src/trails/{topo-verify.ts → validate.ts} +7 -7
- package/src/trails/version-lifecycle-support.ts +945 -0
- package/src/trails/warden-guide.ts +8 -0
- package/src/trails/warden.ts +18 -2
- package/src/versions.ts +4 -1
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import {
|
|
24
24
|
ontrailsPackageRange,
|
|
25
25
|
scaffoldDependencyVersions,
|
|
26
|
+
trailsPackageVersion,
|
|
26
27
|
} from '../versions.js';
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
@@ -39,6 +40,22 @@ interface ScaffoldResult {
|
|
|
39
40
|
readonly plannedOperations: PlannedProjectOperation[];
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
const frameworkCommandScripts = {
|
|
44
|
+
add: 'trails add',
|
|
45
|
+
compile: 'trails compile',
|
|
46
|
+
completions: 'trails completions',
|
|
47
|
+
deprecate: 'trails deprecate',
|
|
48
|
+
diff: 'trails diff',
|
|
49
|
+
doctor: 'trails doctor',
|
|
50
|
+
guide: 'trails guide',
|
|
51
|
+
revise: 'trails revise',
|
|
52
|
+
run: 'trails run',
|
|
53
|
+
survey: 'trails survey',
|
|
54
|
+
topo: 'trails topo',
|
|
55
|
+
validate: 'trails validate',
|
|
56
|
+
warden: 'trails warden',
|
|
57
|
+
} as const satisfies Record<string, string>;
|
|
58
|
+
|
|
42
59
|
// ---------------------------------------------------------------------------
|
|
43
60
|
// Content generators
|
|
44
61
|
// ---------------------------------------------------------------------------
|
|
@@ -55,6 +72,7 @@ const generatePackageJson = (name: string): string => {
|
|
|
55
72
|
),
|
|
56
73
|
devDependencies: Object.fromEntries(
|
|
57
74
|
Object.entries({
|
|
75
|
+
'@ontrails/trails': ontrailsPackageRange,
|
|
58
76
|
'@types/bun': scaffoldDependencyVersions.bunTypes,
|
|
59
77
|
oxfmt: scaffoldDependencyVersions.oxfmt,
|
|
60
78
|
oxlint: scaffoldDependencyVersions.oxlint,
|
|
@@ -63,14 +81,17 @@ const generatePackageJson = (name: string): string => {
|
|
|
63
81
|
}).toSorted(([a], [b]) => a.localeCompare(b))
|
|
64
82
|
),
|
|
65
83
|
name,
|
|
66
|
-
scripts:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
scripts: Object.fromEntries(
|
|
85
|
+
Object.entries({
|
|
86
|
+
build: 'tsc -b',
|
|
87
|
+
'format:check': 'bunx ultracite check .',
|
|
88
|
+
'format:fix': 'bunx ultracite fix .',
|
|
89
|
+
lint: 'oxlint ./src',
|
|
90
|
+
test: 'bun test',
|
|
91
|
+
typecheck: 'tsc --noEmit',
|
|
92
|
+
...frameworkCommandScripts,
|
|
93
|
+
}).toSorted(([a], [b]) => a.localeCompare(b))
|
|
94
|
+
),
|
|
74
95
|
type: 'module',
|
|
75
96
|
version: '0.1.0',
|
|
76
97
|
};
|
|
@@ -78,6 +99,18 @@ const generatePackageJson = (name: string): string => {
|
|
|
78
99
|
return JSON.stringify(pkg, null, 2);
|
|
79
100
|
};
|
|
80
101
|
|
|
102
|
+
const generateScaffoldProvenance = (starter: Starter): string =>
|
|
103
|
+
JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
generatedAt: new Date().toISOString(),
|
|
106
|
+
scaffoldVersion: trailsPackageVersion,
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
template: starter,
|
|
109
|
+
},
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
);
|
|
113
|
+
|
|
81
114
|
const TSCONFIG_CONTENT = JSON.stringify(
|
|
82
115
|
{
|
|
83
116
|
compilerOptions: {
|
|
@@ -98,6 +131,74 @@ const TSCONFIG_CONTENT = JSON.stringify(
|
|
|
98
131
|
2
|
|
99
132
|
);
|
|
100
133
|
|
|
134
|
+
const TSCONFIG_TESTS_CONTENT = JSON.stringify(
|
|
135
|
+
{
|
|
136
|
+
compilerOptions: {
|
|
137
|
+
noEmit: true,
|
|
138
|
+
rootDir: '.',
|
|
139
|
+
types: ['bun'],
|
|
140
|
+
},
|
|
141
|
+
exclude: [],
|
|
142
|
+
extends: './tsconfig.json',
|
|
143
|
+
include: ['src', '__tests__'],
|
|
144
|
+
},
|
|
145
|
+
null,
|
|
146
|
+
2
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const AGENTS_CONTENT = `# AGENTS.md
|
|
150
|
+
|
|
151
|
+
This is a Trails project. Trails is an agent-native, contract-first TypeScript framework: author a trail once with typed input, Result output, examples, intent, and meta; surface it through CLI, MCP, HTTP, or future WebSocket without rewriting the contract.
|
|
152
|
+
|
|
153
|
+
## Commands
|
|
154
|
+
|
|
155
|
+
Use the project scripts first:
|
|
156
|
+
|
|
157
|
+
\`\`\`bash
|
|
158
|
+
bun install
|
|
159
|
+
bun run build
|
|
160
|
+
bun test
|
|
161
|
+
bun run typecheck
|
|
162
|
+
bun run lint
|
|
163
|
+
bun run format:check
|
|
164
|
+
bun run warden
|
|
165
|
+
bun run survey
|
|
166
|
+
bun run guide
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Lexicon
|
|
170
|
+
|
|
171
|
+
- \`trail\`, not action or handler
|
|
172
|
+
- \`blaze\`, not handler or impl
|
|
173
|
+
- \`topo\`, not registry or collection
|
|
174
|
+
- \`compose\`, not follow
|
|
175
|
+
- \`surface\`, not transport
|
|
176
|
+
- \`resource\`, not service or dependency
|
|
177
|
+
- \`layer\`, for compose-cutting trail wrapping
|
|
178
|
+
|
|
179
|
+
## Trail Rules
|
|
180
|
+
|
|
181
|
+
- Blazes return \`Result\`; never throw from trail logic.
|
|
182
|
+
- Use \`Result.ok()\` and \`Result.err()\`; branch with \`isOk()\`, \`isErr()\`, or \`match()\`.
|
|
183
|
+
- Keep trail logic surface-agnostic. Do not import CLI, MCP, HTTP, request, or response types into blazes.
|
|
184
|
+
- Public MCP or HTTP trails declare an \`output\` schema.
|
|
185
|
+
- Trails that compose other trails declare \`composes: [...]\` and invoke them with \`ctx.compose(...)\`.
|
|
186
|
+
- Trails that use infrastructure declare \`resources: [...]\` and access them through the resource helpers.
|
|
187
|
+
- Use \`detours\` for recovery strategies instead of inline retry logic.
|
|
188
|
+
- Prefer examples for happy-path coverage, and add focused tests for edge cases.
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
const CLAUDE_CONTENT = `# CLAUDE.md
|
|
192
|
+
|
|
193
|
+
## Compatibility Shim
|
|
194
|
+
|
|
195
|
+
Keep shared project guidance in \`./AGENTS.md\`. Only Claude-specific bootstrap notes belong here.
|
|
196
|
+
|
|
197
|
+
## Agent Instructions
|
|
198
|
+
|
|
199
|
+
@AGENTS.md
|
|
200
|
+
`;
|
|
201
|
+
|
|
101
202
|
const GITIGNORE_CONTENT = `node_modules/
|
|
102
203
|
dist/
|
|
103
204
|
*.tsbuildinfo
|
|
@@ -112,6 +213,15 @@ import ultracite from 'ultracite/oxlint/core';
|
|
|
112
213
|
|
|
113
214
|
export default defineConfig({
|
|
114
215
|
extends: [ultracite],
|
|
216
|
+
rules: {
|
|
217
|
+
'no-warning-comments': [
|
|
218
|
+
'error',
|
|
219
|
+
{
|
|
220
|
+
location: 'start',
|
|
221
|
+
terms: ['todo:', 'fixme', 'xxx'],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
115
225
|
});
|
|
116
226
|
`;
|
|
117
227
|
|
|
@@ -153,9 +263,13 @@ export const hello = trail('hello', {
|
|
|
153
263
|
`;
|
|
154
264
|
|
|
155
265
|
const generateEntityTrails = (): string =>
|
|
156
|
-
`import {
|
|
266
|
+
`import { randomUUID } from 'node:crypto';
|
|
267
|
+
|
|
268
|
+
import { NotFoundError, Result, trail } from '@ontrails/core';
|
|
157
269
|
import { z } from 'zod';
|
|
158
270
|
|
|
271
|
+
import { entityStore } from '../store.js';
|
|
272
|
+
|
|
159
273
|
const entitySchema = z.object({
|
|
160
274
|
id: z.string(),
|
|
161
275
|
name: z.string(),
|
|
@@ -170,28 +284,85 @@ export const show = trail('entity.show', {
|
|
|
170
284
|
name: 'Show entity',
|
|
171
285
|
},
|
|
172
286
|
],
|
|
173
|
-
blaze: (input) => {
|
|
174
|
-
|
|
287
|
+
blaze: (input, ctx) => {
|
|
288
|
+
const store = entityStore.from(ctx);
|
|
289
|
+
const entity = store.get(input.id);
|
|
290
|
+
if (!entity) {
|
|
291
|
+
return Result.err(new NotFoundError(\`Entity "\${input.id}" not found\`));
|
|
292
|
+
}
|
|
293
|
+
return Result.ok(entity);
|
|
175
294
|
},
|
|
176
295
|
input: z.object({ id: z.string() }),
|
|
177
296
|
output: entitySchema,
|
|
178
297
|
intent: 'read',
|
|
298
|
+
resources: [entityStore],
|
|
179
299
|
});
|
|
180
300
|
|
|
181
301
|
export const add = trail('entity.add', {
|
|
182
302
|
description: 'Add a new entity',
|
|
183
303
|
examples: [
|
|
184
304
|
{
|
|
185
|
-
|
|
305
|
+
expectedMatch: { name: 'New' },
|
|
186
306
|
input: { name: 'New' },
|
|
187
307
|
name: 'Add entity',
|
|
188
308
|
},
|
|
189
309
|
],
|
|
190
|
-
blaze: (input) => {
|
|
191
|
-
|
|
310
|
+
blaze: (input, ctx) => {
|
|
311
|
+
const store = entityStore.from(ctx);
|
|
312
|
+
const entity = { id: randomUUID(), name: input.name };
|
|
313
|
+
store.add(entity);
|
|
314
|
+
return Result.ok(entity);
|
|
192
315
|
},
|
|
193
316
|
input: z.object({ name: z.string() }),
|
|
194
317
|
output: entitySchema,
|
|
318
|
+
intent: 'write',
|
|
319
|
+
permit: { scopes: ['entity:write'] },
|
|
320
|
+
resources: [entityStore],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
export const list = trail('entity.list', {
|
|
324
|
+
description: 'List entities',
|
|
325
|
+
examples: [
|
|
326
|
+
{
|
|
327
|
+
expected: { entities: [{ id: '1', name: 'Example' }] },
|
|
328
|
+
input: {},
|
|
329
|
+
name: 'List entities',
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
blaze: (_input, ctx) => {
|
|
333
|
+
const store = entityStore.from(ctx);
|
|
334
|
+
return Result.ok({ entities: store.list() });
|
|
335
|
+
},
|
|
336
|
+
input: z.object({}),
|
|
337
|
+
output: z.object({
|
|
338
|
+
entities: z.array(entitySchema),
|
|
339
|
+
}),
|
|
340
|
+
intent: 'read',
|
|
341
|
+
resources: [entityStore],
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
export const remove = trail('entity.delete', {
|
|
345
|
+
description: 'Delete an entity by ID',
|
|
346
|
+
examples: [
|
|
347
|
+
{
|
|
348
|
+
expected: { deleted: true, id: '1' },
|
|
349
|
+
input: { id: '1' },
|
|
350
|
+
name: 'Delete entity',
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
blaze: (input, ctx) => {
|
|
354
|
+
const store = entityStore.from(ctx);
|
|
355
|
+
const deleted = store.delete(input.id);
|
|
356
|
+
return Result.ok({ deleted, id: input.id });
|
|
357
|
+
},
|
|
358
|
+
input: z.object({ id: z.string() }),
|
|
359
|
+
output: z.object({
|
|
360
|
+
deleted: z.boolean(),
|
|
361
|
+
id: z.string(),
|
|
362
|
+
}),
|
|
363
|
+
intent: 'destroy',
|
|
364
|
+
permit: { scopes: ['entity:write'] },
|
|
365
|
+
resources: [entityStore],
|
|
195
366
|
});
|
|
196
367
|
`;
|
|
197
368
|
|
|
@@ -225,9 +396,9 @@ import { z } from 'zod';
|
|
|
225
396
|
|
|
226
397
|
export const onboard = trail('entity.onboard', {
|
|
227
398
|
description: 'Onboard a new entity end-to-end',
|
|
228
|
-
|
|
399
|
+
composes: ['entity.add'],
|
|
229
400
|
blaze: async (input, ctx) => {
|
|
230
|
-
const result = await ctx.
|
|
401
|
+
const result = await ctx.compose('entity.add', { name: input.name });
|
|
231
402
|
if (result.isErr()) {
|
|
232
403
|
return result;
|
|
233
404
|
}
|
|
@@ -235,6 +406,8 @@ export const onboard = trail('entity.onboard', {
|
|
|
235
406
|
},
|
|
236
407
|
input: z.object({ name: z.string() }),
|
|
237
408
|
output: z.object({ onboarded: z.boolean() }),
|
|
409
|
+
intent: 'write',
|
|
410
|
+
permit: { scopes: ['entity:write'] },
|
|
238
411
|
});
|
|
239
412
|
`;
|
|
240
413
|
|
|
@@ -252,21 +425,49 @@ export const entityUpdated = signal('entity.updated', {
|
|
|
252
425
|
`;
|
|
253
426
|
|
|
254
427
|
const generateStore = (): string =>
|
|
255
|
-
|
|
428
|
+
`import { Result, resource } from '@ontrails/core';
|
|
256
429
|
|
|
257
|
-
|
|
430
|
+
/** In-memory store for entities. */
|
|
431
|
+
|
|
432
|
+
export interface Entity {
|
|
258
433
|
readonly id: string;
|
|
259
434
|
readonly name: string;
|
|
260
435
|
}
|
|
261
436
|
|
|
262
|
-
|
|
437
|
+
export interface EntityStore {
|
|
438
|
+
add(entity: Entity): void;
|
|
439
|
+
delete(id: string): boolean;
|
|
440
|
+
get(id: string): Entity | undefined;
|
|
441
|
+
list(): Entity[];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const defaultEntities: readonly Entity[] = [{ id: '1', name: 'Example' }];
|
|
263
445
|
|
|
264
|
-
export const
|
|
265
|
-
|
|
266
|
-
|
|
446
|
+
export const createEntityStore = (
|
|
447
|
+
seed: readonly Entity[] = defaultEntities
|
|
448
|
+
): EntityStore => {
|
|
449
|
+
const store = new Map(seed.map((entity) => [entity.id, entity] as const));
|
|
450
|
+
return {
|
|
451
|
+
add(entity) {
|
|
452
|
+
store.set(entity.id, entity);
|
|
453
|
+
},
|
|
454
|
+
delete(id) {
|
|
455
|
+
return store.delete(id);
|
|
456
|
+
},
|
|
457
|
+
get(id) {
|
|
458
|
+
return store.get(id);
|
|
459
|
+
},
|
|
460
|
+
list() {
|
|
461
|
+
return Array.from(store.values());
|
|
462
|
+
},
|
|
463
|
+
};
|
|
267
464
|
};
|
|
268
|
-
|
|
269
|
-
export const
|
|
465
|
+
|
|
466
|
+
export const entityStore = resource('entity.store', {
|
|
467
|
+
description: 'In-memory entity store for the entity starter.',
|
|
468
|
+
create: () => Result.ok(createEntityStore()),
|
|
469
|
+
mock: createEntityStore,
|
|
470
|
+
});
|
|
270
471
|
`;
|
|
271
472
|
|
|
272
473
|
const starterImports: Record<
|
|
@@ -280,8 +481,9 @@ const starterImports: Record<
|
|
|
280
481
|
"import * as search from './trails/search.js';",
|
|
281
482
|
"import * as onboard from './trails/onboard.js';",
|
|
282
483
|
"import * as entitySignals from './signals/entity-signals.js';",
|
|
484
|
+
"import * as store from './store.js';",
|
|
283
485
|
],
|
|
284
|
-
modules: ['entity', 'search', 'onboard', 'entitySignals'],
|
|
486
|
+
modules: ['entity', 'search', 'onboard', 'entitySignals', 'store'],
|
|
285
487
|
},
|
|
286
488
|
hello: {
|
|
287
489
|
imports: ["import * as hello from './trails/hello.js';"],
|
|
@@ -328,11 +530,15 @@ const collectScaffoldFiles = (
|
|
|
328
530
|
): Map<string, string> =>
|
|
329
531
|
new Map([
|
|
330
532
|
['package.json', generatePackageJson(name)],
|
|
533
|
+
['AGENTS.md', AGENTS_CONTENT],
|
|
534
|
+
['CLAUDE.md', CLAUDE_CONTENT],
|
|
331
535
|
['tsconfig.json', TSCONFIG_CONTENT],
|
|
536
|
+
['tsconfig.tests.json', TSCONFIG_TESTS_CONTENT],
|
|
332
537
|
['.gitignore', GITIGNORE_CONTENT],
|
|
333
538
|
['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
|
|
334
539
|
['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
|
|
335
540
|
['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
|
|
541
|
+
['.trails/scaffold.json', generateScaffoldProvenance(starter)],
|
|
336
542
|
['src/app.ts', generateAppTs(name, starter)],
|
|
337
543
|
...starterFileGenerators[starter](),
|
|
338
544
|
]);
|
|
@@ -354,7 +560,7 @@ export const createScaffold = trail('create.scaffold', {
|
|
|
354
560
|
blaze: async (input) => {
|
|
355
561
|
const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
|
|
356
562
|
if (projectDirResult.isErr()) {
|
|
357
|
-
return
|
|
563
|
+
return projectDirResult;
|
|
358
564
|
}
|
|
359
565
|
|
|
360
566
|
const projectDir = projectDirResult.value;
|
|
@@ -363,13 +569,19 @@ export const createScaffold = trail('create.scaffold', {
|
|
|
363
569
|
const fileMap = collectScaffoldFiles(input.name, starter);
|
|
364
570
|
const operations = collectScaffoldOperations(fileMap);
|
|
365
571
|
const plannedOperations = dryRun
|
|
366
|
-
? planProjectOperations(projectDir, operations)
|
|
367
|
-
: await applyProjectOperations(projectDir, operations
|
|
572
|
+
? planProjectOperations(projectDir, operations, { existing: 'preserve' })
|
|
573
|
+
: await applyProjectOperations(projectDir, operations, {
|
|
574
|
+
existing: 'preserve',
|
|
575
|
+
});
|
|
368
576
|
if (plannedOperations.isErr()) {
|
|
369
577
|
return Result.err(plannedOperations.error);
|
|
370
578
|
}
|
|
371
579
|
|
|
372
|
-
const created = dryRun
|
|
580
|
+
const created = dryRun
|
|
581
|
+
? []
|
|
582
|
+
: plannedOperations.value
|
|
583
|
+
.filter((operation) => operation.kind === 'write')
|
|
584
|
+
.map((operation) => operation.path);
|
|
373
585
|
|
|
374
586
|
return Result.ok({
|
|
375
587
|
created,
|
|
@@ -395,6 +607,7 @@ export const createScaffold = trail('create.scaffold', {
|
|
|
395
607
|
.default('hello')
|
|
396
608
|
.describe('Starter trail'),
|
|
397
609
|
}),
|
|
610
|
+
intent: 'write',
|
|
398
611
|
output: z.object({
|
|
399
612
|
created: z
|
|
400
613
|
.array(z.string())
|
|
@@ -414,5 +627,6 @@ export const createScaffold = trail('create.scaffold', {
|
|
|
414
627
|
])
|
|
415
628
|
),
|
|
416
629
|
}),
|
|
630
|
+
permit: { scopes: ['project:write'] },
|
|
417
631
|
visibility: 'internal',
|
|
418
632
|
});
|
package/src/trails/create.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `create`
|
|
2
|
+
* `create` trail -- Create a new Trails project.
|
|
3
3
|
*
|
|
4
4
|
* Composes create.scaffold, add.surface, and add.verify sub-trails
|
|
5
|
-
* via ctx.
|
|
5
|
+
* via ctx.compose.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { InternalError, Result, trail } from '@ontrails/core';
|
|
9
|
+
import type { TrailContext } from '@ontrails/core';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
PROJECT_NAME_MESSAGE,
|
|
13
14
|
PROJECT_NAME_PATTERN,
|
|
15
|
+
projectPathExists,
|
|
16
|
+
writeProjectFile,
|
|
14
17
|
} from '../project-writes.js';
|
|
15
18
|
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
@@ -47,10 +50,17 @@ interface ScaffoldedProject {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
interface SurfaceResult {
|
|
50
|
-
readonly created: string;
|
|
53
|
+
readonly created: string | null;
|
|
51
54
|
readonly dependency: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
type TrailContextWithCompose = TrailContext & {
|
|
58
|
+
readonly compose: NonNullable<TrailContext['compose']>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const hasCompose = (ctx: TrailContext): ctx is TrailContextWithCompose =>
|
|
62
|
+
Boolean(ctx.compose);
|
|
63
|
+
|
|
54
64
|
const buildScaffoldInput = (input: ScaffoldRequest) => ({
|
|
55
65
|
...(input.dir === undefined ? {} : { dir: input.dir }),
|
|
56
66
|
name: input.name,
|
|
@@ -78,7 +88,9 @@ const collectSurfaceFiles = async (
|
|
|
78
88
|
if (result.isErr()) {
|
|
79
89
|
return Result.err(result.error);
|
|
80
90
|
}
|
|
81
|
-
|
|
91
|
+
if (result.value.created !== null) {
|
|
92
|
+
created.push(result.value.created);
|
|
93
|
+
}
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
return Result.ok(created);
|
|
@@ -101,26 +113,104 @@ const collectVerifyFiles = async (
|
|
|
101
113
|
const collectCreatedFiles = (
|
|
102
114
|
scaffolded: readonly string[],
|
|
103
115
|
surfaces: readonly string[],
|
|
104
|
-
verify: readonly string[]
|
|
105
|
-
|
|
116
|
+
verify: readonly string[],
|
|
117
|
+
readme: string | null
|
|
118
|
+
): string[] =>
|
|
119
|
+
readme === null
|
|
120
|
+
? [...scaffolded, ...surfaces, ...verify]
|
|
121
|
+
: [...scaffolded, ...surfaces, ...verify, readme];
|
|
122
|
+
|
|
123
|
+
const surfaceReadmeLines = {
|
|
124
|
+
cli: '- `src/cli.ts` - CLI surface entry point',
|
|
125
|
+
http: '- `src/http.ts` - HTTP surface entry point',
|
|
126
|
+
mcp: '- `src/mcp.ts` - MCP surface entry point',
|
|
127
|
+
} satisfies Record<Surface, string>;
|
|
128
|
+
|
|
129
|
+
const starterReadmeLines = {
|
|
130
|
+
empty:
|
|
131
|
+
'Starts with an empty `src/trails/` directory for authoring from scratch.',
|
|
132
|
+
entity:
|
|
133
|
+
'Includes sample entity trails, a signal, and an in-memory store for exploration.',
|
|
134
|
+
hello: 'Includes a `hello` trail with examples for the first happy path.',
|
|
135
|
+
} satisfies Record<Starter, string>;
|
|
136
|
+
|
|
137
|
+
const generateReadme = (input: CreateInput): string => {
|
|
138
|
+
const surfaceLines = input.surfaces
|
|
139
|
+
.map((surface) => surfaceReadmeLines[surface])
|
|
140
|
+
.join('\n');
|
|
141
|
+
const verificationCommand = input.verify ? 'bun test\n' : '';
|
|
142
|
+
const verificationStructure = input.verify
|
|
143
|
+
? '- `__tests__/examples.test.ts` - examples-as-tests harness\n'
|
|
144
|
+
: '- Verification files were not generated for this project\n';
|
|
145
|
+
|
|
146
|
+
return `# ${input.name}
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
## Getting Started
|
|
151
|
+
|
|
152
|
+
\`\`\`bash
|
|
153
|
+
bun install
|
|
154
|
+
${verificationCommand}bun run warden
|
|
155
|
+
bun run survey
|
|
156
|
+
bun run guide
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
## Project Structure
|
|
160
|
+
|
|
161
|
+
- \`src/app.ts\` - the topo that collects this project's trails
|
|
162
|
+
- \`src/trails/\` - trail definitions
|
|
163
|
+
${surfaceLines}
|
|
164
|
+
${verificationStructure}- \`AGENTS.md\` - project guidance for agents working in this app
|
|
165
|
+
|
|
166
|
+
## Starter
|
|
167
|
+
|
|
168
|
+
${starterReadmeLines[input.starter]}
|
|
169
|
+
|
|
170
|
+
## Next Steps
|
|
171
|
+
|
|
172
|
+
- Add a trail with \`bun run add\`
|
|
173
|
+
- Run \`bun run warden\` before review
|
|
174
|
+
- Read \`AGENTS.md\` for Trails vocabulary and conventions
|
|
175
|
+
`;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const writeReadme = async (
|
|
179
|
+
input: CreateInput,
|
|
180
|
+
dir: string
|
|
181
|
+
): Promise<Result<string | null, Error>> => {
|
|
182
|
+
const exists = projectPathExists(dir, 'README.md');
|
|
183
|
+
if (exists.isErr()) {
|
|
184
|
+
return Result.err(exists.error);
|
|
185
|
+
}
|
|
186
|
+
if (exists.value) {
|
|
187
|
+
return Result.ok(null);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const written = await writeProjectFile(
|
|
191
|
+
dir,
|
|
192
|
+
'README.md',
|
|
193
|
+
generateReadme(input)
|
|
194
|
+
);
|
|
195
|
+
return written.isErr() ? Result.err(written.error) : Result.ok('README.md');
|
|
196
|
+
};
|
|
106
197
|
|
|
107
198
|
// ---------------------------------------------------------------------------
|
|
108
|
-
//
|
|
199
|
+
// Trail definition
|
|
109
200
|
// ---------------------------------------------------------------------------
|
|
110
201
|
|
|
111
|
-
export const
|
|
202
|
+
export const createTrail = trail('create', {
|
|
112
203
|
blaze: async (input: CreateInput, ctx) => {
|
|
113
|
-
if (!ctx
|
|
114
|
-
return Result.err(new InternalError('create
|
|
204
|
+
if (!hasCompose(ctx)) {
|
|
205
|
+
return Result.err(new InternalError('create trail requires ctx.compose'));
|
|
115
206
|
}
|
|
116
|
-
const { cross } = ctx;
|
|
117
207
|
|
|
118
|
-
const scaffolded = await
|
|
208
|
+
const scaffolded = await ctx.compose<ScaffoldedProject>(
|
|
119
209
|
'create.scaffold',
|
|
120
210
|
buildScaffoldInput(input)
|
|
121
211
|
);
|
|
122
212
|
if (scaffolded.isErr()) {
|
|
123
|
-
return
|
|
213
|
+
return scaffolded;
|
|
124
214
|
}
|
|
125
215
|
|
|
126
216
|
const finishCreate = async (): Promise<
|
|
@@ -129,7 +219,7 @@ export const createRoute = trail('create', {
|
|
|
129
219
|
const surfaceFiles = await collectSurfaceFiles(
|
|
130
220
|
input.surfaces,
|
|
131
221
|
(surface) =>
|
|
132
|
-
|
|
222
|
+
ctx.compose<SurfaceResult>(
|
|
133
223
|
'add.surface',
|
|
134
224
|
buildSurfaceInput(scaffolded.value.dir, surface)
|
|
135
225
|
)
|
|
@@ -139,17 +229,26 @@ export const createRoute = trail('create', {
|
|
|
139
229
|
}
|
|
140
230
|
|
|
141
231
|
const verifyFiles = await collectVerifyFiles(input.verify, () =>
|
|
142
|
-
|
|
232
|
+
ctx.compose<{ created: string[] }>(
|
|
233
|
+
'add.verify',
|
|
234
|
+
buildVerifyInput(input)
|
|
235
|
+
)
|
|
143
236
|
);
|
|
144
237
|
if (verifyFiles.isErr()) {
|
|
145
238
|
return Result.err(verifyFiles.error);
|
|
146
239
|
}
|
|
147
240
|
|
|
241
|
+
const readmeFile = await writeReadme(input, scaffolded.value.dir);
|
|
242
|
+
if (readmeFile.isErr()) {
|
|
243
|
+
return Result.err(readmeFile.error);
|
|
244
|
+
}
|
|
245
|
+
|
|
148
246
|
return Result.ok({
|
|
149
247
|
created: collectCreatedFiles(
|
|
150
248
|
scaffolded.value.created,
|
|
151
249
|
surfaceFiles.value,
|
|
152
|
-
verifyFiles.value
|
|
250
|
+
verifyFiles.value,
|
|
251
|
+
readmeFile.value
|
|
153
252
|
),
|
|
154
253
|
dir: scaffolded.value.dir,
|
|
155
254
|
name: input.name,
|
|
@@ -158,7 +257,7 @@ export const createRoute = trail('create', {
|
|
|
158
257
|
|
|
159
258
|
return finishCreate();
|
|
160
259
|
},
|
|
161
|
-
|
|
260
|
+
composes: ['create.scaffold', 'add.surface', 'add.verify'],
|
|
162
261
|
description: 'Create a new Trails project',
|
|
163
262
|
fields: {
|
|
164
263
|
starter: {
|
|
@@ -204,6 +303,7 @@ export const createRoute = trail('create', {
|
|
|
204
303
|
.describe('Starter trail'),
|
|
205
304
|
surfaces: z
|
|
206
305
|
.array(z.enum(['cli', 'http', 'mcp']))
|
|
306
|
+
.min(1)
|
|
207
307
|
.default(['cli'])
|
|
208
308
|
.describe('Surfaces'),
|
|
209
309
|
verify: z.boolean().default(true).describe('Include testing + warden'),
|
|
@@ -213,4 +313,5 @@ export const createRoute = trail('create', {
|
|
|
213
313
|
dir: z.string(),
|
|
214
314
|
name: z.string(),
|
|
215
315
|
}),
|
|
316
|
+
permit: { scopes: ['project:write'] },
|
|
216
317
|
});
|