@ontrails/trails 1.0.0-beta.21 → 1.0.0-beta.23

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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # trails
2
2
 
3
+ ## 1.0.0-beta.23
4
+
5
+ ### Patch Changes
6
+
7
+ - 7c037a0: Allow `trails release check` to pass as a no-op in generated single-package apps
8
+ that do not declare package workspaces.
9
+ - Updated dependencies [9c5ecdc]
10
+ - @ontrails/http@1.0.0-beta.23
11
+ - @ontrails/commander@1.0.0-beta.23
12
+ - @ontrails/adapter-kit@1.0.0-beta.23
13
+ - @ontrails/cli@1.0.0-beta.23
14
+ - @ontrails/core@1.0.0-beta.23
15
+ - @ontrails/mcp@1.0.0-beta.23
16
+ - @ontrails/observe@1.0.0-beta.23
17
+ - @ontrails/permits@1.0.0-beta.23
18
+ - @ontrails/topographer@1.0.0-beta.23
19
+ - @ontrails/tracing@1.0.0-beta.23
20
+ - @ontrails/warden@1.0.0-beta.23
21
+ - @ontrails/wayfinder@1.0.0-beta.23
22
+
23
+ ## 1.0.0-beta.22
24
+
25
+ ### Patch Changes
26
+
27
+ - cdee4d0: Emit formatter-clean fresh scaffold files so generated apps pass their own
28
+ `format:check` script before any manual cleanup.
29
+ - @ontrails/commander@1.0.0-beta.22
30
+ - @ontrails/adapter-kit@1.0.0-beta.22
31
+ - @ontrails/cli@1.0.0-beta.22
32
+ - @ontrails/core@1.0.0-beta.22
33
+ - @ontrails/http@1.0.0-beta.22
34
+ - @ontrails/mcp@1.0.0-beta.22
35
+ - @ontrails/observe@1.0.0-beta.22
36
+ - @ontrails/permits@1.0.0-beta.22
37
+ - @ontrails/topographer@1.0.0-beta.22
38
+ - @ontrails/tracing@1.0.0-beta.22
39
+ - @ontrails/warden@1.0.0-beta.22
40
+ - @ontrails/wayfinder@1.0.0-beta.22
41
+
3
42
  ## 1.0.0-beta.21
4
43
 
5
44
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/trails",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.23",
4
4
  "bin": {
5
5
  "trails": "./bin/trails.ts"
6
6
  },
@@ -27,23 +27,23 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.1.0",
30
- "@ontrails/adapter-kit": "^1.0.0-beta.21",
31
- "@ontrails/cli": "^1.0.0-beta.21",
32
- "@ontrails/commander": "^1.0.0-beta.21",
33
- "@ontrails/core": "^1.0.0-beta.21",
34
- "@ontrails/http": "^1.0.0-beta.21",
35
- "@ontrails/mcp": "^1.0.0-beta.21",
36
- "@ontrails/observe": "^1.0.0-beta.21",
37
- "@ontrails/permits": "^1.0.0-beta.21",
38
- "@ontrails/topographer": "^1.0.0-beta.21",
39
- "@ontrails/tracing": "^1.0.0-beta.21",
40
- "@ontrails/warden": "^1.0.0-beta.21",
41
- "@ontrails/wayfinder": "^1.0.0-beta.21",
30
+ "@ontrails/adapter-kit": "^1.0.0-beta.23",
31
+ "@ontrails/cli": "^1.0.0-beta.23",
32
+ "@ontrails/commander": "^1.0.0-beta.23",
33
+ "@ontrails/core": "^1.0.0-beta.23",
34
+ "@ontrails/http": "^1.0.0-beta.23",
35
+ "@ontrails/mcp": "^1.0.0-beta.23",
36
+ "@ontrails/observe": "^1.0.0-beta.23",
37
+ "@ontrails/permits": "^1.0.0-beta.23",
38
+ "@ontrails/topographer": "^1.0.0-beta.23",
39
+ "@ontrails/tracing": "^1.0.0-beta.23",
40
+ "@ontrails/warden": "^1.0.0-beta.23",
41
+ "@ontrails/wayfinder": "^1.0.0-beta.23",
42
42
  "commander": "^14.0.3",
43
43
  "typescript": "^5.9.3",
44
44
  "zod": "^4.3.5"
45
45
  },
46
46
  "devDependencies": {
47
- "@ontrails/testing": "^1.0.0-beta.21"
47
+ "@ontrails/testing": "^1.0.0-beta.23"
48
48
  }
49
49
  }
@@ -170,7 +170,7 @@ export const discoverWorkspaces = async (
170
170
  const root = await readJson<PackageJson>(join(repoRoot, 'package.json'));
171
171
 
172
172
  if (!root.workspaces || root.workspaces.length === 0) {
173
- throw new Error('Root package.json has no workspaces field');
173
+ return [];
174
174
  }
175
175
 
176
176
  const dirs = await discoverWorkspaceDirs(repoRoot, root.workspaces);
@@ -756,9 +756,19 @@ export const runReleaseCheck = async (
756
756
  const baseRef =
757
757
  options.baseRef ??
758
758
  (options.changedFilesPath === undefined ? 'origin/main' : undefined);
759
- const changedFiles = options.changedFilesPath
760
- ? readChangedFiles(options.changedFilesPath)
761
- : readLocalChangedFiles(options.repoRoot, baseRef ?? 'origin/main');
759
+ let changedFiles: readonly string[];
760
+
761
+ if (options.changedFilesPath !== undefined) {
762
+ changedFiles = readChangedFiles(options.changedFilesPath);
763
+ } else if (workspaces.length > 0) {
764
+ changedFiles = readLocalChangedFiles(
765
+ options.repoRoot,
766
+ baseRef ?? 'origin/main'
767
+ );
768
+ } else {
769
+ changedFiles = [];
770
+ }
771
+
762
772
  const loadedConfig = await loadReleaseConfig({
763
773
  ...(options.configPath === undefined
764
774
  ? {}
@@ -17,6 +17,7 @@ import {
17
17
  } from '../project-writes.js';
18
18
  import { ontrailsPackageRange } from '../versions.js';
19
19
  import { findTopoPath } from './project.js';
20
+ import { stringifyScaffoldPackageJson } from './scaffold-json.js';
20
21
 
21
22
  type Surface = 'cli' | 'http' | 'mcp';
22
23
 
@@ -107,7 +108,7 @@ const updatePkgJsonForSurface = async (
107
108
  const written = await writeProjectFile(
108
109
  cwd,
109
110
  'package.json',
110
- `${JSON.stringify(pkg, null, 2)}\n`
111
+ stringifyScaffoldPackageJson(pkg)
111
112
  );
112
113
  return written.isErr() ? Result.err(written.error) : Result.ok(depName);
113
114
  };
@@ -19,6 +19,7 @@ import {
19
19
  ontrailsPackageRange,
20
20
  scaffoldDependencyVersions,
21
21
  } from '../versions.js';
22
+ import { stringifyScaffoldPackageJson } from './scaffold-json.js';
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Content generators
@@ -81,7 +82,7 @@ const updatePackageJsonForVerify = async (
81
82
  const written = await writeProjectFile(
82
83
  projectDir,
83
84
  'package.json',
84
- `${JSON.stringify(pkg, null, 2)}\n`
85
+ stringifyScaffoldPackageJson(pkg)
85
86
  );
86
87
  return written.isErr() ? Result.err(written.error) : Result.ok();
87
88
  };
@@ -25,6 +25,10 @@ import {
25
25
  scaffoldDependencyVersions,
26
26
  trailsPackageVersion,
27
27
  } from '../versions.js';
28
+ import {
29
+ stringifyScaffoldJson,
30
+ stringifyScaffoldPackageJson,
31
+ } from './scaffold-json.js';
28
32
 
29
33
  // ---------------------------------------------------------------------------
30
34
  // Types
@@ -96,55 +100,45 @@ const generatePackageJson = (name: string): string => {
96
100
  version: '0.1.0',
97
101
  };
98
102
 
99
- return JSON.stringify(pkg, null, 2);
103
+ return stringifyScaffoldPackageJson(pkg);
100
104
  };
101
105
 
102
106
  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
-
114
- const TSCONFIG_CONTENT = JSON.stringify(
115
- {
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'],
107
+ stringifyScaffoldJson({
108
+ generatedAt: new Date().toISOString(),
109
+ scaffoldVersion: trailsPackageVersion,
110
+ schemaVersion: 1,
111
+ template: starter,
112
+ });
113
+
114
+ const TSCONFIG_CONTENT = `{
115
+ "compilerOptions": {
116
+ "declaration": true,
117
+ "module": "ESNext",
118
+ "moduleResolution": "bundler",
119
+ "noUncheckedIndexedAccess": true,
120
+ "outDir": "dist",
121
+ "rootDir": "src",
122
+ "skipLibCheck": true,
123
+ "strict": true,
124
+ "target": "ESNext",
125
+ "verbatimModuleSyntax": true
129
126
  },
130
- null,
131
- 2
132
- );
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__'],
127
+ "include": ["src"]
128
+ }
129
+ `;
130
+
131
+ const TSCONFIG_TESTS_CONTENT = `{
132
+ "compilerOptions": {
133
+ "noEmit": true,
134
+ "rootDir": ".",
135
+ "types": ["bun"]
144
136
  },
145
- null,
146
- 2
147
- );
137
+ "exclude": [],
138
+ "extends": "./tsconfig.json",
139
+ "include": ["src", "__tests__"]
140
+ }
141
+ `;
148
142
 
149
143
  const AGENTS_CONTENT = `# AGENTS.md
150
144
 
@@ -226,7 +220,16 @@ export default defineConfig({
226
220
  `;
227
221
 
228
222
  const OXFMTRC_CONTENT = `{
229
- // ultracite defaults
223
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
224
+ "tabWidth": 2,
225
+ "useTabs": false,
226
+ "semi": true,
227
+ "singleQuote": true,
228
+ "trailingComma": "es5",
229
+ "bracketSpacing": true,
230
+ "arrowParens": "always",
231
+ "proseWrap": "never",
232
+ "printWidth": 80,
230
233
  }
231
234
  `;
232
235
 
@@ -235,6 +238,10 @@ const generateHelloTrail = (): string =>
235
238
  import { z } from 'zod';
236
239
 
237
240
  export const hello = trail('hello', {
241
+ blaze: (input) => {
242
+ const name = input.name ?? 'world';
243
+ return Result.ok({ message: \`Hello, \${name}!\` });
244
+ },
238
245
  description: 'Say hello',
239
246
  examples: [
240
247
  {
@@ -248,17 +255,13 @@ export const hello = trail('hello', {
248
255
  name: 'Named greeting',
249
256
  },
250
257
  ],
251
- blaze: (input) => {
252
- const name = input.name ?? 'world';
253
- return Result.ok({ message: \`Hello, \${name}!\` });
254
- },
255
258
  input: z.object({
256
259
  name: z.string().optional(),
257
260
  }),
261
+ intent: 'read',
258
262
  output: z.object({
259
263
  message: z.string(),
260
264
  }),
261
- intent: 'read',
262
265
  });
263
266
  `;
264
267
 
@@ -276,14 +279,6 @@ const entitySchema = z.object({
276
279
  });
277
280
 
278
281
  export const show = trail('entity.show', {
279
- description: 'Show an entity by ID',
280
- examples: [
281
- {
282
- expected: { id: '1', name: 'Example' },
283
- input: { id: '1' },
284
- name: 'Show entity',
285
- },
286
- ],
287
282
  blaze: (input, ctx) => {
288
283
  const store = entityStore.from(ctx);
289
284
  const entity = store.get(input.id);
@@ -292,13 +287,27 @@ export const show = trail('entity.show', {
292
287
  }
293
288
  return Result.ok(entity);
294
289
  },
290
+ description: 'Show an entity by ID',
291
+ examples: [
292
+ {
293
+ expected: { id: '1', name: 'Example' },
294
+ input: { id: '1' },
295
+ name: 'Show entity',
296
+ },
297
+ ],
295
298
  input: z.object({ id: z.string() }),
296
- output: entitySchema,
297
299
  intent: 'read',
300
+ output: entitySchema,
298
301
  resources: [entityStore],
299
302
  });
300
303
 
301
304
  export const add = trail('entity.add', {
305
+ blaze: (input, ctx) => {
306
+ const store = entityStore.from(ctx);
307
+ const entity = { id: randomUUID(), name: input.name };
308
+ store.add(entity);
309
+ return Result.ok(entity);
310
+ },
302
311
  description: 'Add a new entity',
303
312
  examples: [
304
313
  {
@@ -307,20 +316,18 @@ export const add = trail('entity.add', {
307
316
  name: 'Add entity',
308
317
  },
309
318
  ],
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);
315
- },
316
319
  input: z.object({ name: z.string() }),
317
- output: entitySchema,
318
320
  intent: 'write',
321
+ output: entitySchema,
319
322
  permit: { scopes: ['entity:write'] },
320
323
  resources: [entityStore],
321
324
  });
322
325
 
323
326
  export const list = trail('entity.list', {
327
+ blaze: (_input, ctx) => {
328
+ const store = entityStore.from(ctx);
329
+ return Result.ok({ entities: store.list() });
330
+ },
324
331
  description: 'List entities',
325
332
  examples: [
326
333
  {
@@ -329,19 +336,20 @@ export const list = trail('entity.list', {
329
336
  name: 'List entities',
330
337
  },
331
338
  ],
332
- blaze: (_input, ctx) => {
333
- const store = entityStore.from(ctx);
334
- return Result.ok({ entities: store.list() });
335
- },
336
339
  input: z.object({}),
340
+ intent: 'read',
337
341
  output: z.object({
338
342
  entities: z.array(entitySchema),
339
343
  }),
340
- intent: 'read',
341
344
  resources: [entityStore],
342
345
  });
343
346
 
344
347
  export const remove = trail('entity.delete', {
348
+ blaze: (input, ctx) => {
349
+ const store = entityStore.from(ctx);
350
+ const deleted = store.delete(input.id);
351
+ return Result.ok({ deleted, id: input.id });
352
+ },
345
353
  description: 'Delete an entity by ID',
346
354
  examples: [
347
355
  {
@@ -350,17 +358,12 @@ export const remove = trail('entity.delete', {
350
358
  name: 'Delete entity',
351
359
  },
352
360
  ],
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
361
  input: z.object({ id: z.string() }),
362
+ intent: 'destroy',
359
363
  output: z.object({
360
364
  deleted: z.boolean(),
361
365
  id: z.string(),
362
366
  }),
363
- intent: 'destroy',
364
367
  permit: { scopes: ['entity:write'] },
365
368
  resources: [entityStore],
366
369
  });
@@ -371,6 +374,9 @@ const generateSearchTrail = (): string =>
371
374
  import { z } from 'zod';
372
375
 
373
376
  export const search = trail('search', {
377
+ blaze: () => {
378
+ return Result.ok({ results: [] });
379
+ },
374
380
  description: 'Search entities by query',
375
381
  examples: [
376
382
  {
@@ -379,14 +385,11 @@ export const search = trail('search', {
379
385
  name: 'Search entities',
380
386
  },
381
387
  ],
382
- blaze: () => {
383
- return Result.ok({ results: [] });
384
- },
385
388
  input: z.object({ query: z.string() }),
389
+ intent: 'read',
386
390
  output: z.object({
387
391
  results: z.array(z.object({ id: z.string(), name: z.string() })),
388
392
  }),
389
- intent: 'read',
390
393
  });
391
394
  `;
392
395
 
@@ -395,8 +398,6 @@ const generateOnboardTrail = (): string =>
395
398
  import { z } from 'zod';
396
399
 
397
400
  export const onboard = trail('entity.onboard', {
398
- description: 'Onboard a new entity end-to-end',
399
- composes: ['entity.add'],
400
401
  blaze: async (input, ctx) => {
401
402
  const result = await ctx.compose('entity.add', { name: input.name });
402
403
  if (result.isErr()) {
@@ -404,9 +405,11 @@ export const onboard = trail('entity.onboard', {
404
405
  }
405
406
  return Result.ok({ onboarded: true });
406
407
  },
408
+ composes: ['entity.add'],
409
+ description: 'Onboard a new entity end-to-end',
407
410
  input: z.object({ name: z.string() }),
408
- output: z.object({ onboarded: z.boolean() }),
409
411
  intent: 'write',
412
+ output: z.object({ onboarded: z.boolean() }),
410
413
  permit: { scopes: ['entity:write'] },
411
414
  });
412
415
  `;
@@ -458,14 +461,14 @@ export const createEntityStore = (
458
461
  return store.get(id);
459
462
  },
460
463
  list() {
461
- return Array.from(store.values());
464
+ return [...store.values()];
462
465
  },
463
466
  };
464
467
  };
465
468
 
466
469
  export const entityStore = resource('entity.store', {
467
- description: 'In-memory entity store for the entity starter.',
468
470
  create: () => Result.ok(createEntityStore()),
471
+ description: 'In-memory entity store for the entity starter.',
469
472
  mock: createEntityStore,
470
473
  });
471
474
  `;
@@ -491,19 +494,31 @@ const starterImports: Record<
491
494
  },
492
495
  };
493
496
 
497
+ const renderTopoExpression = (
498
+ appNameLiteral: string,
499
+ modules: readonly string[]
500
+ ): string => {
501
+ if (modules.length === 0) {
502
+ return `topo(${appNameLiteral})`;
503
+ }
504
+
505
+ if (modules.length === 1) {
506
+ return `topo(${appNameLiteral}, ${modules[0]})`;
507
+ }
508
+
509
+ return `topo(\n ${[appNameLiteral, ...modules].join(',\n ')}\n)`;
510
+ };
511
+
494
512
  const generateAppTs = (name: string, starter: Starter): string => {
495
513
  const { imports, modules } = starterImports[starter];
496
- const appNameLiteral = JSON.stringify(name);
497
- const topoArgs =
498
- modules.length > 0
499
- ? `${appNameLiteral}, ${modules.join(', ')}`
500
- : appNameLiteral;
514
+ const appNameLiteral = `'${name}'`;
515
+ const topoExpression = renderTopoExpression(appNameLiteral, modules);
501
516
 
502
517
  return [
503
518
  "import { topo } from '@ontrails/core';",
504
519
  ...imports,
505
520
  '',
506
- `export const app = topo(${topoArgs});`,
521
+ `export const app = ${topoExpression};`,
507
522
  '',
508
523
  ].join('\n');
509
524
  };
@@ -0,0 +1,58 @@
1
+ const packageKeyOrder = [
2
+ 'name',
3
+ 'version',
4
+ 'bin',
5
+ 'type',
6
+ 'scripts',
7
+ 'dependencies',
8
+ 'devDependencies',
9
+ ] as const;
10
+
11
+ const packageMapKeys = new Set<string>([
12
+ 'bin',
13
+ 'dependencies',
14
+ 'devDependencies',
15
+ 'scripts',
16
+ ]);
17
+
18
+ export type ScaffoldPackageJson = Record<string, unknown>;
19
+
20
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
21
+ typeof value === 'object' && value !== null && !Array.isArray(value);
22
+
23
+ const sortRecord = (record: Record<string, unknown>): Record<string, unknown> =>
24
+ Object.fromEntries(
25
+ Object.entries(record).toSorted(([left], [right]) =>
26
+ left.localeCompare(right)
27
+ )
28
+ );
29
+
30
+ const normalizePackageValue = (key: string, value: unknown): unknown =>
31
+ packageMapKeys.has(key) && isPlainRecord(value) ? sortRecord(value) : value;
32
+
33
+ export const normalizeScaffoldPackageJson = (
34
+ pkg: ScaffoldPackageJson
35
+ ): ScaffoldPackageJson => {
36
+ const normalized: ScaffoldPackageJson = {};
37
+
38
+ for (const key of packageKeyOrder) {
39
+ if (pkg[key] !== undefined) {
40
+ normalized[key] = normalizePackageValue(key, pkg[key]);
41
+ }
42
+ }
43
+
44
+ for (const key of Object.keys(pkg).toSorted()) {
45
+ if (!(key in normalized)) {
46
+ normalized[key] = normalizePackageValue(key, pkg[key]);
47
+ }
48
+ }
49
+
50
+ return normalized;
51
+ };
52
+
53
+ export const stringifyScaffoldJson = (value: unknown): string =>
54
+ `${JSON.stringify(value, null, 2)}\n`;
55
+
56
+ export const stringifyScaffoldPackageJson = (
57
+ pkg: ScaffoldPackageJson
58
+ ): string => stringifyScaffoldJson(normalizeScaffoldPackageJson(pkg));