@ontrails/trails 1.0.0-beta.4 → 1.0.0-beta.42

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +1106 -4
  2. package/README.md +27 -0
  3. package/package.json +31 -7
  4. package/src/app.ts +147 -3
  5. package/src/cli.ts +345 -11
  6. package/src/completions.ts +240 -0
  7. package/src/lifecycle-source-io.ts +33 -0
  8. package/src/load-app-mirror.ts +202 -0
  9. package/src/local-state-io.ts +173 -0
  10. package/src/mcp-app.ts +42 -0
  11. package/src/mcp-options.ts +91 -0
  12. package/src/mcp.ts +8 -0
  13. package/src/project-writes.ts +377 -0
  14. package/src/regrade/config.ts +152 -0
  15. package/src/regrade/history.ts +430 -0
  16. package/src/regrade/live-api-preserve.ts +8 -0
  17. package/src/regrade/plan-artifact.ts +285 -0
  18. package/src/release/bindings.ts +39 -0
  19. package/src/release/check.ts +844 -0
  20. package/src/release/config.ts +63 -0
  21. package/src/release/contract-facts.ts +425 -0
  22. package/src/release/index.ts +142 -0
  23. package/src/release/lock-roundtrip-smoke.ts +230 -0
  24. package/src/release/native-bun-publish.ts +651 -0
  25. package/src/release/native-bun-registry.ts +765 -0
  26. package/src/release/notes-cli.ts +171 -0
  27. package/src/release/notes.ts +390 -0
  28. package/src/release/pack-coherence.ts +455 -0
  29. package/src/release/packed-artifacts-smoke.ts +236 -0
  30. package/src/release/policy.ts +1687 -0
  31. package/src/release/semver.ts +104 -0
  32. package/src/release/smoke.ts +56 -0
  33. package/src/release/wayfinder-dogfood-smoke.ts +760 -0
  34. package/src/retired-topo-command.ts +36 -0
  35. package/src/run-adapter-check.ts +76 -0
  36. package/src/run-collision.ts +126 -0
  37. package/src/run-completions-install.ts +179 -0
  38. package/src/run-example.ts +149 -0
  39. package/src/run-examples.ts +148 -0
  40. package/src/run-quiet.ts +75 -0
  41. package/src/run-release-check.ts +74 -0
  42. package/src/run-schema.ts +74 -0
  43. package/src/run-trace.ts +273 -0
  44. package/src/run-warden.ts +39 -0
  45. package/src/run-watch.ts +432 -0
  46. package/src/run-wayfind-outline.ts +170 -0
  47. package/src/scaffold-version-sync.ts +183 -0
  48. package/src/scaffold-versions.generated.ts +12 -0
  49. package/src/trails/adapter-check.ts +244 -0
  50. package/src/trails/add-surface.ts +99 -45
  51. package/src/trails/add-trail.ts +84 -37
  52. package/src/trails/add-verify.ts +100 -30
  53. package/src/trails/compile.ts +58 -0
  54. package/src/trails/completions-complete.ts +165 -0
  55. package/src/trails/completions.ts +47 -0
  56. package/src/trails/create-adapter.ts +785 -0
  57. package/src/trails/create-scaffold.ts +401 -106
  58. package/src/trails/create-versions.ts +62 -0
  59. package/src/trails/create.ts +186 -72
  60. package/src/trails/deprecate.ts +59 -0
  61. package/src/trails/dev-clean.ts +82 -0
  62. package/src/trails/dev-reset.ts +50 -0
  63. package/src/trails/dev-stats.ts +72 -0
  64. package/src/trails/dev-support.ts +360 -0
  65. package/src/trails/doctor.ts +77 -0
  66. package/src/trails/draft-promote.ts +949 -0
  67. package/src/trails/guide.ts +70 -68
  68. package/src/trails/load-app.ts +1123 -15
  69. package/src/trails/operator-context.ts +66 -0
  70. package/src/trails/project.ts +17 -3
  71. package/src/trails/regrade.ts +3091 -0
  72. package/src/trails/release-check.ts +105 -0
  73. package/src/trails/release-smoke.ts +49 -0
  74. package/src/trails/revise.ts +53 -0
  75. package/src/trails/root-dir.ts +21 -0
  76. package/src/trails/run-example.ts +475 -0
  77. package/src/trails/run-examples.ts +129 -0
  78. package/src/trails/run.ts +434 -0
  79. package/src/trails/scaffold-json.ts +58 -0
  80. package/src/trails/survey.ts +877 -214
  81. package/src/trails/topo-activation.ts +14 -0
  82. package/src/trails/topo-constants.ts +2 -0
  83. package/src/trails/topo-history.ts +47 -0
  84. package/src/trails/topo-output-schemas.ts +259 -0
  85. package/src/trails/topo-pin.ts +38 -0
  86. package/src/trails/topo-read-support.ts +368 -0
  87. package/src/trails/topo-reports.ts +809 -0
  88. package/src/trails/topo-store-support.ts +325 -0
  89. package/src/trails/topo-support.ts +220 -0
  90. package/src/trails/topo-unpin.ts +61 -0
  91. package/src/trails/topo.ts +92 -0
  92. package/src/trails/validate.ts +27 -0
  93. package/src/trails/version-lifecycle-support.ts +936 -0
  94. package/src/trails/warden-guide.ts +134 -0
  95. package/src/trails/warden.ts +198 -58
  96. package/src/trails/wayfind-outline.ts +876 -0
  97. package/src/trails/wayfind.ts +1053 -0
  98. package/src/versions.ts +31 -0
  99. package/.turbo/turbo-build.log +0 -1
  100. package/.turbo/turbo-lint.log +0 -3
  101. package/.turbo/turbo-typecheck.log +0 -1
  102. package/__tests__/examples.test.ts +0 -6
  103. package/dist/bin/trails.d.ts +0 -3
  104. package/dist/bin/trails.d.ts.map +0 -1
  105. package/dist/bin/trails.js +0 -4
  106. package/dist/bin/trails.js.map +0 -1
  107. package/dist/src/app.d.ts +0 -2
  108. package/dist/src/app.d.ts.map +0 -1
  109. package/dist/src/app.js +0 -11
  110. package/dist/src/app.js.map +0 -1
  111. package/dist/src/clack.d.ts +0 -9
  112. package/dist/src/clack.d.ts.map +0 -1
  113. package/dist/src/clack.js +0 -84
  114. package/dist/src/clack.js.map +0 -1
  115. package/dist/src/cli.d.ts +0 -2
  116. package/dist/src/cli.d.ts.map +0 -1
  117. package/dist/src/cli.js +0 -13
  118. package/dist/src/cli.js.map +0 -1
  119. package/dist/src/trails/add-surface.d.ts +0 -13
  120. package/dist/src/trails/add-surface.d.ts.map +0 -1
  121. package/dist/src/trails/add-surface.js +0 -88
  122. package/dist/src/trails/add-surface.js.map +0 -1
  123. package/dist/src/trails/add-trail.d.ts +0 -10
  124. package/dist/src/trails/add-trail.d.ts.map +0 -1
  125. package/dist/src/trails/add-trail.js +0 -77
  126. package/dist/src/trails/add-trail.js.map +0 -1
  127. package/dist/src/trails/add-verify.d.ts +0 -10
  128. package/dist/src/trails/add-verify.d.ts.map +0 -1
  129. package/dist/src/trails/add-verify.js +0 -67
  130. package/dist/src/trails/add-verify.js.map +0 -1
  131. package/dist/src/trails/create-scaffold.d.ts +0 -15
  132. package/dist/src/trails/create-scaffold.d.ts.map +0 -1
  133. package/dist/src/trails/create-scaffold.js +0 -288
  134. package/dist/src/trails/create-scaffold.js.map +0 -1
  135. package/dist/src/trails/create.d.ts +0 -22
  136. package/dist/src/trails/create.d.ts.map +0 -1
  137. package/dist/src/trails/create.js +0 -121
  138. package/dist/src/trails/create.js.map +0 -1
  139. package/dist/src/trails/guide.d.ts +0 -11
  140. package/dist/src/trails/guide.d.ts.map +0 -1
  141. package/dist/src/trails/guide.js +0 -80
  142. package/dist/src/trails/guide.js.map +0 -1
  143. package/dist/src/trails/load-app.d.ts +0 -4
  144. package/dist/src/trails/load-app.d.ts.map +0 -1
  145. package/dist/src/trails/load-app.js +0 -24
  146. package/dist/src/trails/load-app.js.map +0 -1
  147. package/dist/src/trails/project.d.ts +0 -8
  148. package/dist/src/trails/project.d.ts.map +0 -1
  149. package/dist/src/trails/project.js +0 -43
  150. package/dist/src/trails/project.js.map +0 -1
  151. package/dist/src/trails/survey.d.ts +0 -31
  152. package/dist/src/trails/survey.d.ts.map +0 -1
  153. package/dist/src/trails/survey.js +0 -221
  154. package/dist/src/trails/survey.js.map +0 -1
  155. package/dist/src/trails/warden.d.ts +0 -19
  156. package/dist/src/trails/warden.d.ts.map +0 -1
  157. package/dist/src/trails/warden.js +0 -88
  158. package/dist/src/trails/warden.js.map +0 -1
  159. package/dist/tsconfig.tsbuildinfo +0 -1
  160. package/src/__tests__/create.test.ts +0 -349
  161. package/src/__tests__/guide.test.ts +0 -91
  162. package/src/__tests__/load-app.test.ts +0 -15
  163. package/src/__tests__/survey.test.ts +0 -159
  164. package/src/__tests__/warden.test.ts +0 -74
  165. package/tsconfig.json +0 -9
@@ -2,50 +2,73 @@
2
2
  * `add.trail` trail -- Scaffold a new trail file with tests.
3
3
  */
4
4
 
5
- import { mkdirSync } from 'node:fs';
6
- import { dirname, join, resolve } from 'node:path';
5
+ import { resolve } from 'node:path';
7
6
 
8
7
  import { Result, trail } from '@ontrails/core';
9
8
  import { z } from 'zod';
10
9
 
10
+ import {
11
+ trailIdToExportName,
12
+ trailIdToModuleName,
13
+ TRAIL_ID_MESSAGE,
14
+ TRAIL_ID_PATTERN,
15
+ validateTrailId,
16
+ writeProjectFile,
17
+ } from '../project-writes.js';
18
+
11
19
  // ---------------------------------------------------------------------------
12
20
  // Helpers
13
21
  // ---------------------------------------------------------------------------
14
22
 
23
+ const literal = (value: string): string => JSON.stringify(value);
24
+
25
+ const deriveExampleMessage = (id: string): string => `${id} completed`;
26
+
15
27
  const generateTrailFile = (
16
28
  id: string,
29
+ description: string,
30
+ exampleName: string,
17
31
  intent: 'read' | 'write' | 'destroy'
18
32
  ): string => {
19
- const intentLine = intent === 'write' ? '' : `\n intent: '${intent}',`;
33
+ const intentLine =
34
+ intent === 'write' ? '' : `\n intent: ${literal(intent)},`;
35
+ const exampleMessage = deriveExampleMessage(id);
36
+ const trailName = trailIdToExportName(id);
20
37
 
21
38
  return `import { Result, trail } from '@ontrails/core';
22
39
  import { z } from 'zod';
23
40
 
24
- export const ${id.replaceAll('.', '_')} = trail('${id}', {
25
- description: 'TODO: describe this trail',
41
+ export const ${trailName} = trail(${literal(id)}, {
42
+ implementation: async () => {
43
+ return Result.ok({ message: ${literal(exampleMessage)} });
44
+ },
45
+ description: ${literal(description)},
26
46
  examples: [
27
47
  {
48
+ expected: { message: ${literal(exampleMessage)} },
28
49
  input: {},
29
- name: 'TODO: add example',
50
+ name: ${literal(exampleName)},
30
51
  },
31
52
  ],
32
- run: async (input) => {
33
- return Result.ok({ message: 'TODO' });
34
- },
35
53
  input: z.object({}),${intentLine}
36
54
  output: z.object({ message: z.string() }),
37
55
  });
38
56
  `;
39
57
  };
40
58
 
41
- const generateTestFile = (id: string): string => {
42
- const moduleName = id.replaceAll('.', '-');
43
- const trailName = id.replaceAll('.', '_');
59
+ const generateTestFile = (id: string, exampleName: string): string => {
60
+ const moduleName = trailIdToModuleName(id);
61
+ const trailName = trailIdToExportName(id);
62
+ const exampleMessage = deriveExampleMessage(id);
44
63
  return `import { testTrail } from '@ontrails/testing';
45
64
  import { ${trailName} } from '../src/trails/${moduleName}.js';
46
65
 
47
66
  testTrail(${trailName}, [
48
- { description: 'basic test', input: {}, expectOk: true },
67
+ {
68
+ description: ${literal(exampleName)},
69
+ expectValue: { message: ${literal(exampleMessage)} },
70
+ input: {},
71
+ },
49
72
  ]);
50
73
  `;
51
74
  };
@@ -54,41 +77,65 @@ testTrail(${trailName}, [
54
77
  // Trail definition
55
78
  // ---------------------------------------------------------------------------
56
79
 
57
- /** Write a file, creating parent directories as needed. */
58
- const writeWithDirs = async (
59
- filePath: string,
60
- content: string
61
- ): Promise<void> => {
62
- mkdirSync(dirname(filePath), { recursive: true });
63
- await Bun.write(filePath, content);
64
- };
65
-
66
80
  export const addTrail = trail('add.trail', {
81
+ args: ['id'],
67
82
  description: 'Scaffold a new trail with tests and examples',
68
- input: z.object({
69
- id: z.string().describe('Trail ID (e.g., entity.update)'),
70
- intent: z
71
- .enum(['read', 'write', 'destroy'])
72
- .default('write')
73
- .describe('Trail intent'),
74
- }),
75
- output: z.object({
76
- created: z.array(z.string()),
77
- }),
78
- run: async (input, ctx) => {
83
+ implementation: async (input, ctx) => {
79
84
  const { id } = input;
80
- const moduleName = id.replaceAll('.', '-');
85
+ const validated = validateTrailId(id);
86
+ if (validated.isErr()) {
87
+ return validated;
88
+ }
89
+
90
+ const moduleName = trailIdToModuleName(validated.value);
81
91
  const cwd = resolve(ctx.cwd ?? '.');
82
92
 
83
93
  const files = new Map<string, string>([
84
- [`src/trails/${moduleName}.ts`, generateTrailFile(id, input.intent)],
85
- [`__tests__/${moduleName}.test.ts`, generateTestFile(id)],
94
+ [
95
+ `src/trails/${moduleName}.ts`,
96
+ generateTrailFile(
97
+ id,
98
+ input.description,
99
+ input.exampleName,
100
+ input.intent
101
+ ),
102
+ ],
103
+ [
104
+ `__tests__/${moduleName}.test.ts`,
105
+ generateTestFile(id, input.exampleName),
106
+ ],
86
107
  ]);
87
108
 
88
109
  for (const [relativePath, content] of files) {
89
- await writeWithDirs(join(cwd, relativePath), content);
110
+ const written = await writeProjectFile(cwd, relativePath, content);
111
+ if (written.isErr()) {
112
+ return written;
113
+ }
90
114
  }
91
115
 
92
116
  return Result.ok({ created: [...files.keys()] });
93
117
  },
118
+ input: z.object({
119
+ description: z
120
+ .string()
121
+ .min(1, 'Trail description is required')
122
+ .describe('Trail description'),
123
+ exampleName: z
124
+ .string()
125
+ .min(1, 'Starter example name is required')
126
+ .describe('Starter example name'),
127
+ id: z
128
+ .string()
129
+ .min(1, 'Trail ID is required')
130
+ .regex(TRAIL_ID_PATTERN, TRAIL_ID_MESSAGE)
131
+ .describe('Trail ID (e.g., entity.update)'),
132
+ intent: z
133
+ .enum(['read', 'write', 'destroy'])
134
+ .default('write')
135
+ .describe('Trail intent'),
136
+ }),
137
+ output: z.object({
138
+ created: z.array(z.string()),
139
+ }),
140
+ permit: { scopes: ['project:write'] },
94
141
  });
@@ -2,36 +2,63 @@
2
2
  * `add.verify` trail -- Add testing + warden setup to a project.
3
3
  */
4
4
 
5
- import { existsSync, mkdirSync } from 'node:fs';
6
- import { dirname, join, resolve } from 'node:path';
5
+ import { existsSync } from 'node:fs';
7
6
 
8
7
  import { Result, trail } from '@ontrails/core';
9
8
  import { z } from 'zod';
10
9
 
10
+ import {
11
+ PROJECT_NAME_MESSAGE,
12
+ PROJECT_NAME_PATTERN,
13
+ projectPathExists,
14
+ resolveProjectDir,
15
+ resolveProjectPath,
16
+ writeProjectFile,
17
+ } from '../project-writes.js';
18
+ import {
19
+ ontrailsPackageRange,
20
+ scaffoldDependencyVersions,
21
+ } from '../versions.js';
22
+ import { stringifyScaffoldPackageJson } from './scaffold-json.js';
23
+
11
24
  // ---------------------------------------------------------------------------
12
25
  // Content generators
13
26
  // ---------------------------------------------------------------------------
14
27
 
15
28
  const generateTestFile = (): string =>
16
- `import { testAll } from '@ontrails/testing';
29
+ `import { testAllEstablished } from '@ontrails/testing/established';
17
30
  import { app } from '../src/app.js';
18
31
 
19
- testAll(app);
32
+ const permitScopes = [
33
+ ...new Set(
34
+ app
35
+ .list()
36
+ .flatMap((trail) =>
37
+ trail.permit && trail.permit !== 'public' ? trail.permit.scopes : []
38
+ )
39
+ ),
40
+ ];
41
+
42
+ testAllEstablished(app, {
43
+ ctx: {
44
+ permit: { id: 'test-permit', scopes: permitScopes },
45
+ },
46
+ });
20
47
  `;
21
48
 
22
49
  const generateLefthookYml = (): string =>
23
50
  `pre-push:
24
51
  commands:
25
52
  warden:
26
- run: bunx trails warden --exit-code
53
+ run: bunx trails warden
27
54
  `;
28
55
 
29
56
  /** Add testing and warden devDependencies to package.json when present. */
30
57
  const patchVerifyDeps = (pkg: Record<string, unknown>): void => {
31
58
  const devDeps = (pkg['devDependencies'] ?? {}) as Record<string, string>;
32
- devDeps['@ontrails/testing'] = 'workspace:*';
33
- devDeps['@ontrails/warden'] = 'workspace:*';
34
- devDeps['lefthook'] = '^2.1.1';
59
+ devDeps['@ontrails/testing'] = ontrailsPackageRange;
60
+ devDeps['@ontrails/warden'] = ontrailsPackageRange;
61
+ devDeps['lefthook'] = scaffoldDependencyVersions.lefthook;
35
62
  pkg['devDependencies'] = Object.fromEntries(
36
63
  Object.entries(devDeps).toSorted(([a], [b]) => a.localeCompare(b))
37
64
  );
@@ -40,14 +67,24 @@ const patchVerifyDeps = (pkg: Record<string, unknown>): void => {
40
67
  /** Update package.json in the target project with verify dependencies. */
41
68
  const updatePackageJsonForVerify = async (
42
69
  projectDir: string
43
- ): Promise<void> => {
44
- const pkgPath = join(projectDir, 'package.json');
70
+ ): Promise<Result<void, Error>> => {
71
+ const pkgPathResult = resolveProjectPath(projectDir, 'package.json');
72
+ if (pkgPathResult.isErr()) {
73
+ return pkgPathResult;
74
+ }
75
+
76
+ const pkgPath = pkgPathResult.value;
45
77
  if (!existsSync(pkgPath)) {
46
- return;
78
+ return Result.ok();
47
79
  }
48
80
  const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
49
81
  patchVerifyDeps(pkg);
50
- await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
82
+ const written = await writeProjectFile(
83
+ projectDir,
84
+ 'package.json',
85
+ stringifyScaffoldPackageJson(pkg)
86
+ );
87
+ return written.isErr() ? Result.err(written.error) : Result.ok();
51
88
  };
52
89
 
53
90
  // ---------------------------------------------------------------------------
@@ -56,32 +93,65 @@ const updatePackageJsonForVerify = async (
56
93
 
57
94
  export const addVerify = trail('add.verify', {
58
95
  description: 'Add testing and warden verification',
59
- input: z.object({
60
- dir: z.string().optional().describe('Parent directory'),
61
- name: z.string().describe('Project name'),
62
- }),
63
- metadata: { internal: true },
64
- output: z.object({
65
- created: z.array(z.string()),
66
- }),
67
- run: async (input) => {
68
- const projectDir = resolve(input.dir ?? '.', input.name);
96
+ implementation: async (input) => {
97
+ const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
98
+ if (projectDirResult.isErr()) {
99
+ return projectDirResult;
100
+ }
101
+
102
+ const projectDir = projectDirResult.value;
69
103
  const files: string[] = [];
70
104
 
71
105
  const writeFile = async (
72
106
  relativePath: string,
73
107
  content: string
74
- ): Promise<void> => {
75
- const fullPath = join(projectDir, relativePath);
76
- mkdirSync(dirname(fullPath), { recursive: true });
77
- await Bun.write(fullPath, content);
78
- files.push(relativePath);
108
+ ): Promise<Result<void, Error>> => {
109
+ const exists = projectPathExists(projectDir, relativePath);
110
+ if (exists.isErr()) {
111
+ return exists;
112
+ }
113
+ if (exists.value) {
114
+ return Result.ok();
115
+ }
116
+
117
+ const written = await writeProjectFile(projectDir, relativePath, content);
118
+ if (written.isErr()) {
119
+ return written;
120
+ }
121
+ files.push(written.value);
122
+ return Result.ok();
79
123
  };
80
124
 
81
- await writeFile('__tests__/examples.test.ts', generateTestFile());
82
- await writeFile('lefthook.yml', generateLefthookYml());
83
- await updatePackageJsonForVerify(projectDir);
125
+ const testFile = await writeFile(
126
+ '__tests__/examples.test.ts',
127
+ generateTestFile()
128
+ );
129
+ if (testFile.isErr()) {
130
+ return testFile;
131
+ }
132
+
133
+ const lefthookFile = await writeFile('lefthook.yml', generateLefthookYml());
134
+ if (lefthookFile.isErr()) {
135
+ return lefthookFile;
136
+ }
137
+
138
+ const packageResult = await updatePackageJsonForVerify(projectDir);
139
+ if (packageResult.isErr()) {
140
+ return packageResult;
141
+ }
84
142
 
85
143
  return Result.ok({ created: files });
86
144
  },
145
+ input: z.object({
146
+ dir: z.string().optional().describe('Parent directory'),
147
+ name: z
148
+ .string()
149
+ .regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
150
+ .describe('Project name'),
151
+ }),
152
+ output: z.object({
153
+ created: z.array(z.string()),
154
+ }),
155
+ permit: { scopes: ['project:write'] },
156
+ visibility: 'internal',
87
157
  });
@@ -0,0 +1,58 @@
1
+ import { trail } from '@ontrails/core';
2
+ import type { Result, Topo } from '@ontrails/core';
3
+ import type { TopoGraphOverlayRegistration } from '@ontrails/topography';
4
+ import { z } from 'zod';
5
+
6
+ import { withFreshOperatorApp } from './operator-context.js';
7
+ import { exportCurrentTopo } from './topo-store-support.js';
8
+ import type { TopoExportReport } from './topo-support.js';
9
+ import {
10
+ createIsolatedExampleInput,
11
+ topoSnapshotOutput,
12
+ } from './topo-support.js';
13
+
14
+ export const compileCurrentTopo = async (
15
+ app: Topo,
16
+ options?: {
17
+ readonly force?: boolean | undefined;
18
+ readonly rootDir?: string;
19
+ readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
20
+ }
21
+ ): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
22
+
23
+ const compileTrailInputSchema = z.object({
24
+ force: z
25
+ .boolean()
26
+ .optional()
27
+ .describe('Record graph-only force events for breaking changes'),
28
+ module: z.string().optional().describe('Path to the app module'),
29
+ rootDir: z.string().optional().describe('Workspace root directory'),
30
+ });
31
+
32
+ type CompileTrailInput = z.output<typeof compileTrailInputSchema>;
33
+
34
+ export const compileTrail = trail('compile', {
35
+ description: 'Compile the current topo to trails.lock',
36
+ examples: [
37
+ {
38
+ input: createIsolatedExampleInput('compile'),
39
+ name: 'Compile the current topo to trails.lock',
40
+ },
41
+ ],
42
+ implementation: async (input: CompileTrailInput, ctx) =>
43
+ withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
44
+ compileCurrentTopo(lease.app, {
45
+ force: input.force,
46
+ overlays: lease.overlays,
47
+ rootDir,
48
+ })
49
+ ),
50
+ input: compileTrailInputSchema,
51
+ intent: 'write',
52
+ output: z.object({
53
+ hash: z.string(),
54
+ lockPath: z.string(),
55
+ snapshot: topoSnapshotOutput,
56
+ }),
57
+ permit: { scopes: ['topo:write'] },
58
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * `completions __complete` internal trail -- dynamic completion suggestions.
3
+ *
4
+ * The static shell scripts emitted by {@link completionsTrail} delegate to this
5
+ * trail at tab-press time. The trail receives the partial argv that the user
6
+ * has typed (after the binary name) and returns newline-delimited suggestions
7
+ * the shell should offer.
8
+ *
9
+ * Today the trail knows about two `run` positions:
10
+ *
11
+ * - `trails run <prefix>` — return matching trail IDs.
12
+ * - `trails run example <trail-id> <prefix>` — return matching example names
13
+ * defined on the resolved trail.
14
+ *
15
+ * The `run example` branch loads the trail's owning app at tab-press time so the
16
+ * suggestions reflect the live trail definition. Unknown trails and no
17
+ * examples naturally collapse to an empty list; recoverable load failures are
18
+ * suppressed here because completion must never surface errors back to the
19
+ * shell mid-keystroke.
20
+ */
21
+
22
+ import { Result, trail } from '@ontrails/core';
23
+ import { z } from 'zod';
24
+
25
+ import {
26
+ renderTrailExampleCompletions,
27
+ renderTrailIdCompletions,
28
+ } from '../completions.js';
29
+ import { resolveTrailRootDir } from './root-dir.js';
30
+
31
+ const EMPTY_SUGGESTIONS = '';
32
+
33
+ interface CompleteContext {
34
+ readonly args: readonly string[];
35
+ readonly rootDir: string;
36
+ }
37
+
38
+ type CompletionHandler = (ctx: CompleteContext) => Promise<readonly string[]>;
39
+
40
+ /**
41
+ * Detect whether the user is completing the example-name positional on a
42
+ * `trails run example` invocation.
43
+ *
44
+ * The shell hands us the partial argv with the **last element** as the token
45
+ * being completed. We recognize the `run example <trail-id> <TAB>` shape when:
46
+ *
47
+ * - the command family is `run example`, and
48
+ * - a non-flag positional (the trail ID) sits at `args[2]`.
49
+ *
50
+ * Returns the trail ID + prefix to complete, or `null` if the cursor is not
51
+ * in an example-name value position.
52
+ */
53
+ const detectExampleValueCompletion = (
54
+ args: readonly string[]
55
+ ): { readonly trailId: string; readonly prefix: string } | null => {
56
+ if (args.length < 4) {
57
+ return null;
58
+ }
59
+ const [, subcommand, trailId] = args;
60
+ if (subcommand !== 'example') {
61
+ return null;
62
+ }
63
+ if (trailId === undefined || trailId.startsWith('-')) {
64
+ return null;
65
+ }
66
+ const prefix = args[3] ?? '';
67
+ return { prefix, trailId };
68
+ };
69
+
70
+ /**
71
+ * Handler for the `trails run` subcommand.
72
+ *
73
+ * Two completion positions are recognized:
74
+ *
75
+ * - `trails run example <trail-id> <prefix>` — return example names defined
76
+ * on the resolved trail (matching `prefix`, sorted).
77
+ * - `trails run <prefix>` — return matching trail IDs.
78
+ *
79
+ * Anything else (unknown flag context, a cursor beyond the trail ID, etc.)
80
+ * returns no suggestions so completed positional values are not suggested
81
+ * again.
82
+ */
83
+ const completeRunPosition: CompletionHandler = async ({ args, rootDir }) => {
84
+ const exampleContext = detectExampleValueCompletion(args);
85
+ if (exampleContext !== null) {
86
+ const suggestionsResult = await renderTrailExampleCompletions(
87
+ rootDir,
88
+ exampleContext.trailId,
89
+ exampleContext.prefix
90
+ );
91
+ return suggestionsResult.unwrapOr([]);
92
+ }
93
+ if (args.length !== 2) {
94
+ return [];
95
+ }
96
+ const prefix = args[1] ?? '';
97
+ return await renderTrailIdCompletions(rootDir, prefix);
98
+ };
99
+
100
+ const renderSuggestions = (suggestions: readonly string[]): string =>
101
+ suggestions.join('\n');
102
+
103
+ /**
104
+ * Subcommand → handler dispatch table.
105
+ *
106
+ * Keep this a pure lookup so adding a new completion target (`run example`,
107
+ * `--app`, etc.) is a new entry rather than a new branch.
108
+ *
109
+ * @remarks As more handlers grow per-token-shape logic (e.g. distinguishing
110
+ * `--app <TAB>` vs `<trail-id> <TAB>` for the same subcommand), expect this
111
+ * table to evolve into a sub-table of (token-pattern → completion-fn) per
112
+ * subcommand or a small parser yielding a discriminated `CompletionContext`
113
+ * union. Today the single `'run'` entry is small enough that explicit
114
+ * branching inside `completeRunPosition` is cleaner.
115
+ */
116
+ const SUBCOMMAND_HANDLERS: Readonly<Record<string, CompletionHandler>> = {
117
+ run: completeRunPosition,
118
+ };
119
+
120
+ export const completionsCompleteTrail = trail('completions.__complete', {
121
+ description:
122
+ 'Internal: emit dynamic completion suggestions for the current partial argv. Invoked by the static shell completion script at tab-press time.',
123
+ examples: [
124
+ {
125
+ description: 'Empty argv yields no suggestions',
126
+ input: { args: [] },
127
+ name: 'Empty args',
128
+ },
129
+ ],
130
+ implementation: async (input, ctx) => {
131
+ const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
132
+ if (rootDirResult.isErr()) {
133
+ return rootDirResult;
134
+ }
135
+ const rootDir = rootDirResult.value;
136
+
137
+ const [subcommand] = input.args;
138
+ if (subcommand === undefined) {
139
+ return Result.ok(EMPTY_SUGGESTIONS);
140
+ }
141
+
142
+ const handler = SUBCOMMAND_HANDLERS[subcommand];
143
+ if (handler === undefined) {
144
+ return Result.ok(EMPTY_SUGGESTIONS);
145
+ }
146
+
147
+ const suggestions = await handler({ args: input.args, rootDir });
148
+ return Result.ok(renderSuggestions(suggestions));
149
+ },
150
+ input: z.object({
151
+ args: z
152
+ .array(z.string())
153
+ .readonly()
154
+ .describe(
155
+ 'Partial argv after the binary name; the last element is the token being completed'
156
+ ),
157
+ rootDir: z.string().optional().describe('Workspace root directory'),
158
+ }),
159
+ intent: 'read',
160
+ output: z
161
+ .string()
162
+ .describe(
163
+ 'Newline-delimited suggestions the shell should offer for the current token'
164
+ ),
165
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `completions` trail -- Print a shell completion script for the `trails` CLI.
3
+ *
4
+ * The trail's responsibility is small: render a static shell script that, when
5
+ * sourced by the user's shell, registers a tab-completion handler that
6
+ * delegates to `trails completions __complete <args...>` for the live
7
+ * suggestions. See {@link renderCompletionScript} for the per-shell shape.
8
+ */
9
+
10
+ import { trail } from '@ontrails/core';
11
+ import { z } from 'zod';
12
+
13
+ import { renderCompletionScript } from '../completions.js';
14
+
15
+ const COMPLETIONS_BIN_NAME = 'trails';
16
+
17
+ export const completionsTrail = trail('completions', {
18
+ args: ['shell'],
19
+ description:
20
+ 'Print a shell completion script for the trails CLI; pipe into your shell rc to register tab-completion',
21
+ examples: [
22
+ {
23
+ description: 'Render a bash completion script',
24
+ input: { shell: 'bash' },
25
+ name: 'Render bash completion',
26
+ },
27
+ {
28
+ description: 'Render a zsh completion script',
29
+ input: { shell: 'zsh' },
30
+ name: 'Render zsh completion',
31
+ },
32
+ {
33
+ description: 'Render a fish completion script',
34
+ input: { shell: 'fish' },
35
+ name: 'Render fish completion',
36
+ },
37
+ ],
38
+ implementation: async (input) =>
39
+ renderCompletionScript(input.shell, COMPLETIONS_BIN_NAME),
40
+ input: z.object({
41
+ shell: z
42
+ .enum(['bash', 'zsh', 'fish'])
43
+ .describe('Target shell flavor for the completion script'),
44
+ }),
45
+ intent: 'read',
46
+ output: z.string(),
47
+ });