@ontrails/trails 0.2.0
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 +1906 -0
- package/README.md +48 -0
- package/bin/trails.ts +3 -0
- package/package.json +57 -0
- package/src/app.ts +167 -0
- package/src/clack.ts +111 -0
- package/src/cli.ts +308 -0
- package/src/completions.ts +431 -0
- package/src/lifecycle-source-io.ts +33 -0
- package/src/load-app-mirror.ts +202 -0
- package/src/local-state-io.ts +129 -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 +515 -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/changeset-packages.ts +99 -0
- package/src/release/check.ts +1191 -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 +221 -0
- package/src/release/index.ts +180 -0
- package/src/release/lock-roundtrip-smoke.ts +255 -0
- package/src/release/lock-roundtrip-workspace.ts +107 -0
- package/src/release/native-bun-publish.ts +964 -0
- package/src/release/native-bun-registry.ts +848 -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 +1780 -0
- package/src/release/semver.ts +104 -0
- package/src/release/smoke.ts +56 -0
- package/src/release/stable-version-release.ts +80 -0
- package/src/release/wayfinder-dogfood-smoke.ts +762 -0
- package/src/release/zero-line-transition.ts +68 -0
- package/src/retired-topo-command.ts +36 -0
- package/src/run-adapter-check.ts +76 -0
- package/src/run-argv.ts +133 -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-project.ts +52 -0
- package/src/run-watch.ts +381 -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 +816 -0
- package/src/trails/add-trail.ts +141 -0
- package/src/trails/add-verify.ts +252 -0
- package/src/trails/compile.ts +118 -0
- package/src/trails/completions-complete.ts +236 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/config-explain.ts +43 -0
- package/src/trails/create-adapter.ts +785 -0
- package/src/trails/create-scaffold.ts +1215 -0
- package/src/trails/create-versions.ts +62 -0
- package/src/trails/create.ts +652 -0
- package/src/trails/deprecate.ts +59 -0
- package/src/trails/dev-clean.ts +80 -0
- package/src/trails/dev-reset.ts +48 -0
- package/src/trails/dev-stats.ts +71 -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 +106 -0
- package/src/trails/load-app.ts +1145 -0
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/project-context-output.ts +304 -0
- package/src/trails/project-context.ts +613 -0
- package/src/trails/project.ts +65 -0
- package/src/trails/regrade.ts +4951 -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 +592 -0
- package/src/trails/run-examples.ts +149 -0
- package/src/trails/run.ts +496 -0
- package/src/trails/scaffold-json.ts +60 -0
- package/src/trails/scaffold-topo-identity.ts +479 -0
- package/src/trails/survey.ts +990 -0
- 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 +247 -0
- package/src/trails/topo-unpin.ts +61 -0
- package/src/trails/topo.ts +92 -0
- package/src/trails/validate.ts +348 -0
- package/src/trails/version-lifecycle-support.ts +936 -0
- package/src/trails/warden-guide.ts +134 -0
- package/src/trails/warden.ts +598 -0
- package/src/trails/wayfind-diff.ts +716 -0
- package/src/trails/wayfind-outline.ts +876 -0
- package/src/trails/wayfind.ts +1319 -0
- package/src/versions.ts +31 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `add.trail` trail -- Scaffold a new trail file with tests.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { Result, trail } from '@ontrails/core';
|
|
8
|
+
import { z } from 'zod';
|
|
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
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const literal = (value: string): string => JSON.stringify(value);
|
|
24
|
+
|
|
25
|
+
const deriveExampleMessage = (id: string): string => `${id} completed`;
|
|
26
|
+
|
|
27
|
+
const generateTrailFile = (
|
|
28
|
+
id: string,
|
|
29
|
+
description: string,
|
|
30
|
+
exampleName: string,
|
|
31
|
+
intent: 'read' | 'write' | 'destroy'
|
|
32
|
+
): string => {
|
|
33
|
+
const intentLine =
|
|
34
|
+
intent === 'write' ? '' : `\n intent: ${literal(intent)},`;
|
|
35
|
+
const exampleMessage = deriveExampleMessage(id);
|
|
36
|
+
const trailName = trailIdToExportName(id);
|
|
37
|
+
|
|
38
|
+
return `import { Result, trail } from '@ontrails/core';
|
|
39
|
+
import { z } from 'zod';
|
|
40
|
+
|
|
41
|
+
export const ${trailName} = trail(${literal(id)}, {
|
|
42
|
+
implementation: async () => {
|
|
43
|
+
return Result.ok({ message: ${literal(exampleMessage)} });
|
|
44
|
+
},
|
|
45
|
+
description: ${literal(description)},
|
|
46
|
+
examples: [
|
|
47
|
+
{
|
|
48
|
+
expected: { message: ${literal(exampleMessage)} },
|
|
49
|
+
input: {},
|
|
50
|
+
name: ${literal(exampleName)},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
input: z.object({}),${intentLine}
|
|
54
|
+
output: z.object({ message: z.string() }),
|
|
55
|
+
});
|
|
56
|
+
`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const generateTestFile = (id: string, exampleName: string): string => {
|
|
60
|
+
const moduleName = trailIdToModuleName(id);
|
|
61
|
+
const trailName = trailIdToExportName(id);
|
|
62
|
+
const exampleMessage = deriveExampleMessage(id);
|
|
63
|
+
return `import { testTrail } from '@ontrails/testing';
|
|
64
|
+
import { ${trailName} } from '../src/trails/${moduleName}.js';
|
|
65
|
+
|
|
66
|
+
testTrail(${trailName}, [
|
|
67
|
+
{
|
|
68
|
+
description: ${literal(exampleName)},
|
|
69
|
+
expectValue: { message: ${literal(exampleMessage)} },
|
|
70
|
+
input: {},
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Trail definition
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export const addTrail = trail('add.trail', {
|
|
81
|
+
args: ['id'],
|
|
82
|
+
description: 'Scaffold a new trail with tests and examples',
|
|
83
|
+
implementation: async (input, ctx) => {
|
|
84
|
+
const { id } = input;
|
|
85
|
+
const validated = validateTrailId(id);
|
|
86
|
+
if (validated.isErr()) {
|
|
87
|
+
return validated;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const moduleName = trailIdToModuleName(validated.value);
|
|
91
|
+
const cwd = resolve(ctx.cwd ?? '.');
|
|
92
|
+
|
|
93
|
+
const files = new Map<string, string>([
|
|
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
|
+
],
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
for (const [relativePath, content] of files) {
|
|
110
|
+
const written = await writeProjectFile(cwd, relativePath, content);
|
|
111
|
+
if (written.isErr()) {
|
|
112
|
+
return written;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return Result.ok({ created: [...files.keys()] });
|
|
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'] },
|
|
141
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `add.verify` trail -- Add testing + warden setup to a project.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { findTrailsConfigPaths } from '@ontrails/config';
|
|
9
|
+
import { InternalError, Result, trail } from '@ontrails/core';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
PROJECT_NAME_MESSAGE,
|
|
14
|
+
PROJECT_NAME_PATTERN,
|
|
15
|
+
projectPathExists,
|
|
16
|
+
resolveProjectDir,
|
|
17
|
+
resolveProjectPath,
|
|
18
|
+
writeProjectFile,
|
|
19
|
+
} from '../project-writes.js';
|
|
20
|
+
import {
|
|
21
|
+
ontrailsPackageRange,
|
|
22
|
+
scaffoldDependencyVersions,
|
|
23
|
+
} from '../versions.js';
|
|
24
|
+
import { resolveOperatorProjectContext } from './project-context.js';
|
|
25
|
+
import { stringifyScaffoldPackageJson } from './scaffold-json.js';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Content generators
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const generateTestFile = (): string =>
|
|
32
|
+
`import { testAllEstablished } from '@ontrails/testing/established';
|
|
33
|
+
import { app } from '../src/app.js';
|
|
34
|
+
|
|
35
|
+
const permitScopes = [
|
|
36
|
+
...new Set(
|
|
37
|
+
app
|
|
38
|
+
.list()
|
|
39
|
+
.flatMap((trail) =>
|
|
40
|
+
trail.permit && trail.permit !== 'public' ? trail.permit.scopes : []
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
testAllEstablished(app, {
|
|
46
|
+
ctx: {
|
|
47
|
+
permit: { id: 'test-permit', scopes: permitScopes },
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const generateLefthookYml = (): string =>
|
|
53
|
+
`pre-push:
|
|
54
|
+
commands:
|
|
55
|
+
warden:
|
|
56
|
+
run: bunx trails warden
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
/** Add testing and warden devDependencies to package.json when present. */
|
|
60
|
+
const patchVerifyDeps = (pkg: Record<string, unknown>): void => {
|
|
61
|
+
const devDeps = (pkg['devDependencies'] ?? {}) as Record<string, string>;
|
|
62
|
+
devDeps['@ontrails/testing'] = ontrailsPackageRange;
|
|
63
|
+
devDeps['@ontrails/warden'] = ontrailsPackageRange;
|
|
64
|
+
devDeps['lefthook'] = scaffoldDependencyVersions.lefthook;
|
|
65
|
+
pkg['devDependencies'] = Object.fromEntries(
|
|
66
|
+
Object.entries(devDeps).toSorted(([a], [b]) => a.localeCompare(b))
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Update package.json in the target project with verify dependencies. */
|
|
71
|
+
const updatePackageJsonForVerify = async (
|
|
72
|
+
projectDir: string
|
|
73
|
+
): Promise<Result<void, Error>> => {
|
|
74
|
+
const pkgPathResult = resolveProjectPath(projectDir, 'package.json');
|
|
75
|
+
if (pkgPathResult.isErr()) {
|
|
76
|
+
return pkgPathResult;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pkgPath = pkgPathResult.value;
|
|
80
|
+
if (!existsSync(pkgPath)) {
|
|
81
|
+
return Result.ok();
|
|
82
|
+
}
|
|
83
|
+
const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
|
|
84
|
+
patchVerifyDeps(pkg);
|
|
85
|
+
const written = await writeProjectFile(
|
|
86
|
+
projectDir,
|
|
87
|
+
'package.json',
|
|
88
|
+
stringifyScaffoldPackageJson(pkg)
|
|
89
|
+
);
|
|
90
|
+
return written.isErr() ? Result.err(written.error) : Result.ok();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const resolveCanonicalHookDir = (
|
|
94
|
+
projectDir: string,
|
|
95
|
+
appRoot: string,
|
|
96
|
+
workspaceRoot: string
|
|
97
|
+
): Result<string, Error> => {
|
|
98
|
+
try {
|
|
99
|
+
if (realpathSync(appRoot) !== realpathSync(projectDir)) {
|
|
100
|
+
return Result.ok(projectDir);
|
|
101
|
+
}
|
|
102
|
+
return Result.ok(realpathSync(workspaceRoot));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return Result.err(
|
|
105
|
+
new InternalError('Failed to resolve add.verify hook ownership.', {
|
|
106
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
107
|
+
context: { appRoot, projectDir, workspaceRoot },
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const canonicalizeProjectedProjectDir = (projectDir: string): string => {
|
|
114
|
+
const missingSegments: string[] = [];
|
|
115
|
+
let existingAncestor = resolve(projectDir);
|
|
116
|
+
while (!existsSync(existingAncestor)) {
|
|
117
|
+
const parent = dirname(existingAncestor);
|
|
118
|
+
if (parent === existingAncestor) {
|
|
119
|
+
return resolve(projectDir);
|
|
120
|
+
}
|
|
121
|
+
missingSegments.unshift(basename(existingAncestor));
|
|
122
|
+
existingAncestor = parent;
|
|
123
|
+
}
|
|
124
|
+
return join(realpathSync(existingAncestor), ...missingSegments);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const hasTrailsConfigAncestor = (projectDir: string): boolean => {
|
|
128
|
+
let current = resolve(projectDir);
|
|
129
|
+
while (true) {
|
|
130
|
+
if (findTrailsConfigPaths(current).length > 0) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const parent = dirname(current);
|
|
134
|
+
if (parent === current) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
current = parent;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Place the hook at a Config-owned workspace root, never a package-only root. */
|
|
142
|
+
export const resolveVerifyHookDir = async (
|
|
143
|
+
projectDir: string
|
|
144
|
+
): Promise<Result<string, Error>> => {
|
|
145
|
+
let contextDir: string;
|
|
146
|
+
try {
|
|
147
|
+
contextDir = canonicalizeProjectedProjectDir(projectDir);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return Result.err(
|
|
150
|
+
new InternalError('Failed to resolve add.verify project context.', {
|
|
151
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
152
|
+
context: { projectDir },
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!hasTrailsConfigAncestor(contextDir)) {
|
|
157
|
+
return Result.ok(projectDir);
|
|
158
|
+
}
|
|
159
|
+
const context = await resolveOperatorProjectContext({}, { cwd: contextDir });
|
|
160
|
+
if (context.isErr()) {
|
|
161
|
+
return context;
|
|
162
|
+
}
|
|
163
|
+
if (context.value.selectedExtent !== 'configured-app') {
|
|
164
|
+
return Result.ok(projectDir);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return resolveCanonicalHookDir(
|
|
168
|
+
projectDir,
|
|
169
|
+
context.value.app.rootDir,
|
|
170
|
+
context.value.projectRoot
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Trail definition
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
export const addVerify = trail('add.verify', {
|
|
179
|
+
description: 'Add testing and warden verification',
|
|
180
|
+
implementation: async (input) => {
|
|
181
|
+
const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
|
|
182
|
+
if (projectDirResult.isErr()) {
|
|
183
|
+
return projectDirResult;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const projectDir = projectDirResult.value;
|
|
187
|
+
const hookDirResult = await resolveVerifyHookDir(projectDir);
|
|
188
|
+
if (hookDirResult.isErr()) {
|
|
189
|
+
return hookDirResult;
|
|
190
|
+
}
|
|
191
|
+
const hookDir = hookDirResult.value;
|
|
192
|
+
const files: string[] = [];
|
|
193
|
+
|
|
194
|
+
const writeFile = async (
|
|
195
|
+
rootDir: string,
|
|
196
|
+
relativePath: string,
|
|
197
|
+
content: string
|
|
198
|
+
): Promise<Result<void, Error>> => {
|
|
199
|
+
const exists = projectPathExists(rootDir, relativePath);
|
|
200
|
+
if (exists.isErr()) {
|
|
201
|
+
return exists;
|
|
202
|
+
}
|
|
203
|
+
if (exists.value) {
|
|
204
|
+
return Result.ok();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const written = await writeProjectFile(rootDir, relativePath, content);
|
|
208
|
+
if (written.isErr()) {
|
|
209
|
+
return written;
|
|
210
|
+
}
|
|
211
|
+
files.push(written.value);
|
|
212
|
+
return Result.ok();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const testFile = await writeFile(
|
|
216
|
+
projectDir,
|
|
217
|
+
'__tests__/examples.test.ts',
|
|
218
|
+
generateTestFile()
|
|
219
|
+
);
|
|
220
|
+
if (testFile.isErr()) {
|
|
221
|
+
return testFile;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const lefthookFile = await writeFile(
|
|
225
|
+
hookDir,
|
|
226
|
+
'lefthook.yml',
|
|
227
|
+
generateLefthookYml()
|
|
228
|
+
);
|
|
229
|
+
if (lefthookFile.isErr()) {
|
|
230
|
+
return lefthookFile;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const packageResult = await updatePackageJsonForVerify(projectDir);
|
|
234
|
+
if (packageResult.isErr()) {
|
|
235
|
+
return packageResult;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return Result.ok({ created: files });
|
|
239
|
+
},
|
|
240
|
+
input: z.object({
|
|
241
|
+
dir: z.string().optional().describe('Parent directory'),
|
|
242
|
+
name: z
|
|
243
|
+
.string()
|
|
244
|
+
.regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
|
|
245
|
+
.describe('Project name'),
|
|
246
|
+
}),
|
|
247
|
+
output: z.object({
|
|
248
|
+
created: z.array(z.string()),
|
|
249
|
+
}),
|
|
250
|
+
permit: { scopes: ['project:write'] },
|
|
251
|
+
visibility: 'internal',
|
|
252
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Result, trail } from '@ontrails/core';
|
|
2
|
+
import type { Topo } from '@ontrails/core';
|
|
3
|
+
import type { TopoGraphOverlayRegistration } from '@ontrails/topography';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { withFreshAppLease } from './operator-context.js';
|
|
7
|
+
import {
|
|
8
|
+
assertConfiguredAppBinding,
|
|
9
|
+
assertObservableProjectApps,
|
|
10
|
+
compileNeedsAppError,
|
|
11
|
+
resolveOperatorAppModuleContext,
|
|
12
|
+
resolveOperatorProjectContext,
|
|
13
|
+
} from './project-context.js';
|
|
14
|
+
import {
|
|
15
|
+
appProjectSelection,
|
|
16
|
+
appProjectSelectionSchema,
|
|
17
|
+
} from './project-context-output.js';
|
|
18
|
+
import { exportCurrentTopo } from './topo-store-support.js';
|
|
19
|
+
import type { TopoExportReport } from './topo-support.js';
|
|
20
|
+
import {
|
|
21
|
+
createCurrentAppExampleInput,
|
|
22
|
+
topoSnapshotOutput,
|
|
23
|
+
} from './topo-support.js';
|
|
24
|
+
|
|
25
|
+
export const compileCurrentTopo = async (
|
|
26
|
+
app: Topo,
|
|
27
|
+
options?: {
|
|
28
|
+
readonly force?: boolean | undefined;
|
|
29
|
+
readonly rootDir?: string;
|
|
30
|
+
readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
|
|
31
|
+
}
|
|
32
|
+
): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
|
|
33
|
+
|
|
34
|
+
const compileTrailInputSchema = z.object({
|
|
35
|
+
app: z.string().optional().describe('Configured workspace app ID'),
|
|
36
|
+
force: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Record graph-only force events for breaking changes'),
|
|
40
|
+
module: z.string().optional().describe('Path to the app module'),
|
|
41
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type CompileTrailInput = z.output<typeof compileTrailInputSchema>;
|
|
45
|
+
|
|
46
|
+
const compileTrailOutputSchema = z.object({
|
|
47
|
+
hash: z.string(),
|
|
48
|
+
lockPath: z.string(),
|
|
49
|
+
project: appProjectSelectionSchema,
|
|
50
|
+
snapshot: topoSnapshotOutput,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
type CompileTrailOutput = z.output<typeof compileTrailOutputSchema>;
|
|
54
|
+
|
|
55
|
+
const compileSelectedProject = async (
|
|
56
|
+
input: CompileTrailInput,
|
|
57
|
+
cwd: string | undefined
|
|
58
|
+
): Promise<Result<CompileTrailOutput, Error>> => {
|
|
59
|
+
const contextResult = await resolveOperatorProjectContext(input, { cwd });
|
|
60
|
+
if (contextResult.isErr()) {
|
|
61
|
+
return contextResult;
|
|
62
|
+
}
|
|
63
|
+
const context = contextResult.value;
|
|
64
|
+
if (context.selectedExtent === 'workspace') {
|
|
65
|
+
return Result.err(compileNeedsAppError(context));
|
|
66
|
+
}
|
|
67
|
+
const appContext = resolveOperatorAppModuleContext(context);
|
|
68
|
+
if (appContext.isErr()) {
|
|
69
|
+
return appContext;
|
|
70
|
+
}
|
|
71
|
+
const selectedContext = appContext.value;
|
|
72
|
+
const observable = await assertObservableProjectApps(context);
|
|
73
|
+
if (observable.isErr()) {
|
|
74
|
+
return observable;
|
|
75
|
+
}
|
|
76
|
+
return withFreshAppLease(
|
|
77
|
+
selectedContext.app.modulePath,
|
|
78
|
+
selectedContext.app.rootDir,
|
|
79
|
+
async (lease) => {
|
|
80
|
+
const binding = assertConfiguredAppBinding(
|
|
81
|
+
selectedContext,
|
|
82
|
+
lease.app.name
|
|
83
|
+
);
|
|
84
|
+
if (binding.isErr()) {
|
|
85
|
+
return binding;
|
|
86
|
+
}
|
|
87
|
+
const compiled = await compileCurrentTopo(lease.app, {
|
|
88
|
+
force: input.force,
|
|
89
|
+
overlays: lease.overlays,
|
|
90
|
+
rootDir: selectedContext.app.rootDir,
|
|
91
|
+
});
|
|
92
|
+
return compiled.isErr()
|
|
93
|
+
? compiled
|
|
94
|
+
: Result.ok({
|
|
95
|
+
...compiled.value,
|
|
96
|
+
project: appProjectSelection(selectedContext, lease.app.name),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const compileTrail = trail('compile', {
|
|
103
|
+
description: 'Compile the current topo to trails.lock',
|
|
104
|
+
examples: [
|
|
105
|
+
{
|
|
106
|
+
input: createCurrentAppExampleInput({
|
|
107
|
+
selection: 'configured-project',
|
|
108
|
+
}),
|
|
109
|
+
name: 'Compile the current topo to trails.lock',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
implementation: async (input: CompileTrailInput, ctx) =>
|
|
113
|
+
compileSelectedProject(input, ctx.cwd),
|
|
114
|
+
input: compileTrailInputSchema,
|
|
115
|
+
intent: 'write',
|
|
116
|
+
output: compileTrailOutputSchema,
|
|
117
|
+
permit: { scopes: ['topo:write'] },
|
|
118
|
+
});
|