@ontrails/trails 1.0.0-beta.24 → 1.0.0-beta.29
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 +189 -0
- package/README.md +6 -5
- package/package.json +16 -14
- package/src/app.ts +42 -3
- package/src/cli.ts +17 -3
- package/src/local-state-io.ts +20 -0
- package/src/mcp-app.ts +10 -0
- package/src/mcp-options.ts +2 -3
- package/src/project-writes.ts +11 -11
- package/src/release/check.ts +41 -25
- package/src/release/index.ts +40 -0
- package/src/release/native-bun-registry.ts +282 -19
- package/src/release/pack-coherence.ts +457 -0
- package/src/release/policy.ts +1684 -0
- package/src/release/semver.ts +104 -0
- package/src/release/wayfinder-dogfood-smoke.ts +600 -66
- package/src/run-completions-install.ts +2 -2
- package/src/run-schema.ts +41 -0
- package/src/run-wayfind-outline.ts +166 -0
- package/src/trails/adapter-check.ts +1 -1
- package/src/trails/add-surface.ts +1 -1
- package/src/trails/add-verify.ts +3 -3
- package/src/trails/compile.ts +17 -25
- package/src/trails/create-scaffold.ts +14 -14
- package/src/trails/create.ts +5 -5
- package/src/trails/dev-support.ts +44 -24
- package/src/trails/doctor.ts +23 -2
- package/src/trails/draft-promote.ts +7 -7
- package/src/trails/guide.ts +15 -19
- package/src/trails/load-app.ts +58 -9
- package/src/trails/operator-context.ts +66 -0
- package/src/trails/regrade.ts +72 -0
- package/src/trails/release-check.ts +1 -0
- package/src/trails/run-example.ts +21 -37
- package/src/trails/run-examples.ts +16 -32
- package/src/trails/run.ts +31 -44
- package/src/trails/survey.ts +76 -62
- package/src/trails/topo-output-schemas.ts +11 -0
- package/src/trails/topo-pin.ts +6 -20
- package/src/trails/topo-read-support.ts +91 -41
- package/src/trails/topo-reports.ts +4 -2
- package/src/trails/topo-store-support.ts +172 -39
- package/src/trails/topo-support.ts +1 -2
- package/src/trails/topo.ts +5 -19
- package/src/trails/validate.ts +9 -20
- package/src/trails/version-lifecycle-support.ts +10 -20
- package/src/trails/warden.ts +18 -0
- package/src/trails/wayfind.ts +1001 -0
|
@@ -100,7 +100,7 @@ export const runCompletionsInstall = async (
|
|
|
100
100
|
): Promise<Result<CompletionsInstallResult, Error>> => {
|
|
101
101
|
const shellResult = resolveTargetShell(options);
|
|
102
102
|
if (shellResult.isErr()) {
|
|
103
|
-
return
|
|
103
|
+
return shellResult;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const shell = shellResult.value;
|
|
@@ -111,7 +111,7 @@ export const runCompletionsInstall = async (
|
|
|
111
111
|
options.binName ?? COMPLETIONS_BIN_NAME
|
|
112
112
|
);
|
|
113
113
|
if (scriptResult.isErr()) {
|
|
114
|
-
return
|
|
114
|
+
return scriptResult;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
let existed: boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CliCommand } from '@ontrails/cli';
|
|
2
|
+
import { deriveCliSchema, findCliSchemaCommand } from '@ontrails/cli';
|
|
3
|
+
import type { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
const normalizeCommandPath = (path: readonly string[] | undefined): string[] =>
|
|
6
|
+
(path ?? []).flatMap((segment) =>
|
|
7
|
+
segment
|
|
8
|
+
.trim()
|
|
9
|
+
.split(/\s+/)
|
|
10
|
+
.filter((part) => part.length > 0)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const writeJson = (value: unknown): void => {
|
|
14
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const attachSchemaCommand = (
|
|
18
|
+
program: Command,
|
|
19
|
+
commands: readonly CliCommand[]
|
|
20
|
+
): void => {
|
|
21
|
+
const schema = deriveCliSchema(commands);
|
|
22
|
+
program
|
|
23
|
+
.command('schema [command...]')
|
|
24
|
+
.description('Inspect accepted CLI command contracts as JSON')
|
|
25
|
+
.action((commandPath: string[] | undefined) => {
|
|
26
|
+
const path = normalizeCommandPath(commandPath);
|
|
27
|
+
if (path.length === 0) {
|
|
28
|
+
writeJson(schema);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const command = findCliSchemaCommand(schema, path);
|
|
33
|
+
if (command === undefined) {
|
|
34
|
+
process.stderr.write(`Unknown CLI command: ${path.join(' ')}\n`);
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
writeJson({ command });
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { deriveOutputMode, output } from '@ontrails/cli';
|
|
2
|
+
import type { ActionResultContext } from '@ontrails/cli';
|
|
3
|
+
import { outlineOutputSchema } from '@ontrails/wayfinder';
|
|
4
|
+
import type { OutlineFeature, OutlineOutput } from '@ontrails/wayfinder';
|
|
5
|
+
|
|
6
|
+
const includesFeature = (
|
|
7
|
+
outline: OutlineOutput,
|
|
8
|
+
feature: OutlineFeature
|
|
9
|
+
): boolean => outline.features.included.includes(feature);
|
|
10
|
+
|
|
11
|
+
const formatLine = (
|
|
12
|
+
line: number,
|
|
13
|
+
kind: string,
|
|
14
|
+
name: string,
|
|
15
|
+
suffix = ''
|
|
16
|
+
): string => `${line.toString().padStart(4, ' ')}: ${kind} ${name}${suffix}`;
|
|
17
|
+
|
|
18
|
+
const appendGraphCount = (lines: string[], outline: OutlineOutput): void => {
|
|
19
|
+
if (
|
|
20
|
+
includesFeature(outline, 'graph') &&
|
|
21
|
+
outline.counts.graphMatches !== undefined
|
|
22
|
+
) {
|
|
23
|
+
lines.push(` graph matches: ${outline.counts.graphMatches.toString()}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const appendDiagnosticCount = (
|
|
28
|
+
lines: string[],
|
|
29
|
+
outline: OutlineOutput
|
|
30
|
+
): void => {
|
|
31
|
+
if (
|
|
32
|
+
includesFeature(outline, 'diagnostics') &&
|
|
33
|
+
outline.counts.diagnostics > 0
|
|
34
|
+
) {
|
|
35
|
+
lines.push(` diagnostics: ${outline.counts.diagnostics.toString()}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const appendSourceDeclarations = (
|
|
40
|
+
lines: string[],
|
|
41
|
+
outline: OutlineOutput
|
|
42
|
+
): void => {
|
|
43
|
+
if (!includesFeature(outline, 'source')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const declarations = outline.source?.declarations ?? [];
|
|
47
|
+
if (declarations.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
lines.push('');
|
|
51
|
+
for (const declaration of declarations.slice(0, 40)) {
|
|
52
|
+
lines.push(
|
|
53
|
+
formatLine(declaration.line, declaration.kind, declaration.name)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type TrailOutline = NonNullable<OutlineOutput['trails']>[number];
|
|
59
|
+
|
|
60
|
+
const contractFactLabel = (
|
|
61
|
+
contracts: TrailOutline['contracts']
|
|
62
|
+
): string | undefined => {
|
|
63
|
+
if (contracts === undefined) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (contracts.input && contracts.output) {
|
|
67
|
+
return 'input+output';
|
|
68
|
+
}
|
|
69
|
+
if (contracts.input) {
|
|
70
|
+
return 'input';
|
|
71
|
+
}
|
|
72
|
+
if (contracts.output) {
|
|
73
|
+
return 'output';
|
|
74
|
+
}
|
|
75
|
+
return 'no schemas';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const exampleCountLabel = (count: number): string =>
|
|
79
|
+
`${count.toString()} ${count === 1 ? 'example' : 'examples'}`;
|
|
80
|
+
|
|
81
|
+
const trailFactSuffix = (trail: TrailOutline): string => {
|
|
82
|
+
const facts = [
|
|
83
|
+
trail.graph?.intent,
|
|
84
|
+
contractFactLabel(trail.contracts),
|
|
85
|
+
trail.graph === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: exampleCountLabel(trail.graph.exampleCount),
|
|
88
|
+
].filter((fact): fact is string => fact !== undefined);
|
|
89
|
+
|
|
90
|
+
return facts.length === 0 ? '' : ` (${facts.join(', ')})`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const appendTrails = (lines: string[], outline: OutlineOutput): void => {
|
|
94
|
+
if (!includesFeature(outline, 'trails')) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const trails = outline.trails ?? [];
|
|
98
|
+
if (trails.length === 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
for (const trail of trails) {
|
|
103
|
+
lines.push(
|
|
104
|
+
formatLine(trail.line, 'trail', trail.id, trailFactSuffix(trail))
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const appendApps = (lines: string[], outline: OutlineOutput): void => {
|
|
110
|
+
if (!includesFeature(outline, 'apps')) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const apps = outline.apps ?? [];
|
|
114
|
+
if (apps.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
lines.push('');
|
|
118
|
+
for (const app of apps) {
|
|
119
|
+
lines.push(formatLine(app.line, 'app', app.name, ` (${app.callee})`));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const appendDiagnostics = (lines: string[], outline: OutlineOutput): void => {
|
|
124
|
+
if (!includesFeature(outline, 'diagnostics')) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const diagnostic of outline.diagnostics ?? []) {
|
|
128
|
+
lines.push(
|
|
129
|
+
` ${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const formatWayfindOutlineText = (outline: OutlineOutput): string => {
|
|
135
|
+
const lines = [
|
|
136
|
+
outline.file,
|
|
137
|
+
` trails: ${outline.counts.trails.toString()}`,
|
|
138
|
+
` apps: ${outline.counts.apps.toString()}`,
|
|
139
|
+
` declarations: ${outline.counts.declarations.toString()}`,
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
appendGraphCount(lines, outline);
|
|
143
|
+
appendDiagnosticCount(lines, outline);
|
|
144
|
+
appendSourceDeclarations(lines, outline);
|
|
145
|
+
appendTrails(lines, outline);
|
|
146
|
+
appendApps(lines, outline);
|
|
147
|
+
appendDiagnostics(lines, outline);
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const tryWayfindOutlineOutput = (ctx: ActionResultContext): boolean => {
|
|
153
|
+
if (ctx.trail.id !== 'wayfind.outline' || ctx.result.isErr()) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const { mode } = deriveOutputMode(ctx.flags, ctx.topoName);
|
|
157
|
+
if (mode !== 'text') {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
const parsed = outlineOutputSchema.safeParse(ctx.result.value);
|
|
161
|
+
if (!parsed.success) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
output(formatWayfindOutlineText(parsed.data), mode);
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
@@ -145,7 +145,7 @@ const validateAdapterCheckRoot = (
|
|
|
145
145
|
|
|
146
146
|
const manifest = readWorkspaceManifest(packageJsonPath);
|
|
147
147
|
if (manifest.isErr()) {
|
|
148
|
-
return
|
|
148
|
+
return manifest;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (workspacePatternsFromManifest(manifest.value).length === 0) {
|
|
@@ -96,7 +96,7 @@ const updatePkgJsonForSurface = async (
|
|
|
96
96
|
): Promise<Result<string, Error>> => {
|
|
97
97
|
const pkgPathResult = resolveProjectPath(cwd, 'package.json');
|
|
98
98
|
if (pkgPathResult.isErr()) {
|
|
99
|
-
return
|
|
99
|
+
return pkgPathResult;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const pkgPath = pkgPathResult.value;
|
package/src/trails/add-verify.ts
CHANGED
|
@@ -70,7 +70,7 @@ const updatePackageJsonForVerify = async (
|
|
|
70
70
|
): Promise<Result<void, Error>> => {
|
|
71
71
|
const pkgPathResult = resolveProjectPath(projectDir, 'package.json');
|
|
72
72
|
if (pkgPathResult.isErr()) {
|
|
73
|
-
return
|
|
73
|
+
return pkgPathResult;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const pkgPath = pkgPathResult.value;
|
|
@@ -107,7 +107,7 @@ export const addVerify = trail('add.verify', {
|
|
|
107
107
|
): Promise<Result<void, Error>> => {
|
|
108
108
|
const exists = projectPathExists(projectDir, relativePath);
|
|
109
109
|
if (exists.isErr()) {
|
|
110
|
-
return
|
|
110
|
+
return exists;
|
|
111
111
|
}
|
|
112
112
|
if (exists.value) {
|
|
113
113
|
return Result.ok();
|
|
@@ -115,7 +115,7 @@ export const addVerify = trail('add.verify', {
|
|
|
115
115
|
|
|
116
116
|
const written = await writeProjectFile(projectDir, relativePath, content);
|
|
117
117
|
if (written.isErr()) {
|
|
118
|
-
return
|
|
118
|
+
return written;
|
|
119
119
|
}
|
|
120
120
|
files.push(written.value);
|
|
121
121
|
return Result.ok();
|
package/src/trails/compile.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { trail } from '@ontrails/core';
|
|
2
|
-
import type { Result, Topo } from '@ontrails/core';
|
|
2
|
+
import type { CliCommandAliasInput, Result, Topo } from '@ontrails/core';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { resolveTrailRootDir } from './root-dir.js';
|
|
5
|
+
import { withFreshOperatorApp } from './operator-context.js';
|
|
7
6
|
import { exportCurrentTopo } from './topo-store-support.js';
|
|
8
7
|
import type { TopoExportReport } from './topo-support.js';
|
|
9
8
|
import {
|
|
@@ -13,7 +12,13 @@ import {
|
|
|
13
12
|
|
|
14
13
|
export const compileCurrentTopo = async (
|
|
15
14
|
app: Topo,
|
|
16
|
-
options?: {
|
|
15
|
+
options?: {
|
|
16
|
+
readonly cliAliases?:
|
|
17
|
+
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
18
|
+
| undefined;
|
|
19
|
+
readonly force?: boolean | undefined;
|
|
20
|
+
readonly rootDir?: string;
|
|
21
|
+
}
|
|
17
22
|
): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
|
|
18
23
|
|
|
19
24
|
const compileTrailInputSchema = z.object({
|
|
@@ -28,31 +33,19 @@ const compileTrailInputSchema = z.object({
|
|
|
28
33
|
type CompileTrailInput = z.output<typeof compileTrailInputSchema>;
|
|
29
34
|
|
|
30
35
|
export const compileTrail = trail('compile', {
|
|
31
|
-
blaze: async (input: CompileTrailInput, ctx) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
const rootDir = rootDirResult.value;
|
|
37
|
-
const leaseResult = await tryLoadFreshAppLease(input.module, rootDir);
|
|
38
|
-
if (leaseResult.isErr()) {
|
|
39
|
-
return leaseResult;
|
|
40
|
-
}
|
|
41
|
-
const lease = leaseResult.value;
|
|
42
|
-
try {
|
|
43
|
-
return await compileCurrentTopo(lease.app, {
|
|
36
|
+
blaze: async (input: CompileTrailInput, ctx) =>
|
|
37
|
+
withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
|
|
38
|
+
compileCurrentTopo(lease.app, {
|
|
39
|
+
cliAliases: lease.cliAliases,
|
|
44
40
|
force: input.force,
|
|
45
41
|
rootDir,
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
description: 'Compile the current topo to .trails artifacts',
|
|
42
|
+
})
|
|
43
|
+
),
|
|
44
|
+
description: 'Compile the current topo to trails.lock',
|
|
52
45
|
examples: [
|
|
53
46
|
{
|
|
54
47
|
input: createIsolatedExampleInput('compile'),
|
|
55
|
-
name: 'Compile the current topo
|
|
48
|
+
name: 'Compile the current topo to trails.lock',
|
|
56
49
|
},
|
|
57
50
|
],
|
|
58
51
|
input: compileTrailInputSchema,
|
|
@@ -61,7 +54,6 @@ export const compileTrail = trail('compile', {
|
|
|
61
54
|
hash: z.string(),
|
|
62
55
|
lockPath: z.string(),
|
|
63
56
|
snapshot: topoSnapshotOutput,
|
|
64
|
-
topoPath: z.string(),
|
|
65
57
|
}),
|
|
66
58
|
permit: { scopes: ['topo:write'] },
|
|
67
59
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `create.scaffold` trail -- Creates base project structure.
|
|
3
3
|
*
|
|
4
|
-
* Generates package.json, tsconfig, app.ts, starter trails, and
|
|
4
|
+
* Generates package.json, tsconfig, app.ts, starter trails, and scaffold provenance.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
|
|
9
|
-
import { Result, trail
|
|
9
|
+
import { Result, trail } from '@ontrails/core';
|
|
10
|
+
import type { Result as TrailsResult } from '@ontrails/core';
|
|
10
11
|
import { z } from 'zod';
|
|
11
12
|
|
|
12
13
|
import {
|
|
@@ -168,7 +169,7 @@ bun run guide
|
|
|
168
169
|
- \`compose\`, not follow
|
|
169
170
|
- \`surface\`, not transport
|
|
170
171
|
- \`resource\`, not service or dependency
|
|
171
|
-
- \`layer\`, for
|
|
172
|
+
- \`layer\`, for cross-cutting trail wrapping
|
|
172
173
|
|
|
173
174
|
## Trail Rules
|
|
174
175
|
|
|
@@ -196,10 +197,7 @@ Keep shared project guidance in \`./AGENTS.md\`. Only Claude-specific bootstrap
|
|
|
196
197
|
const GITIGNORE_CONTENT = `node_modules/
|
|
197
198
|
dist/
|
|
198
199
|
*.tsbuildinfo
|
|
199
|
-
.
|
|
200
|
-
.trails/state/
|
|
201
|
-
.trails/config.local.js
|
|
202
|
-
.trails/config.local.ts
|
|
200
|
+
trails.config.local.*
|
|
203
201
|
`;
|
|
204
202
|
|
|
205
203
|
const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
|
|
@@ -552,7 +550,6 @@ const collectScaffoldFiles = (
|
|
|
552
550
|
['.gitignore', GITIGNORE_CONTENT],
|
|
553
551
|
['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
|
|
554
552
|
['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
|
|
555
|
-
['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
|
|
556
553
|
['.trails/scaffold.json', generateScaffoldProvenance(starter)],
|
|
557
554
|
['src/app.ts', generateAppTs(name, starter)],
|
|
558
555
|
...starterFileGenerators[starter](),
|
|
@@ -583,13 +580,16 @@ export const createScaffold = trail('create.scaffold', {
|
|
|
583
580
|
const dryRun = input.dryRun === true;
|
|
584
581
|
const fileMap = collectScaffoldFiles(input.name, starter);
|
|
585
582
|
const operations = collectScaffoldOperations(fileMap);
|
|
586
|
-
const plannedOperations =
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
583
|
+
const plannedOperations: TrailsResult<PlannedProjectOperation[], Error> =
|
|
584
|
+
dryRun
|
|
585
|
+
? planProjectOperations(projectDir, operations, {
|
|
586
|
+
existing: 'preserve',
|
|
587
|
+
})
|
|
588
|
+
: await applyProjectOperations(projectDir, operations, {
|
|
589
|
+
existing: 'preserve',
|
|
590
|
+
});
|
|
591
591
|
if (plannedOperations.isErr()) {
|
|
592
|
-
return
|
|
592
|
+
return plannedOperations;
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
const created = dryRun
|
package/src/trails/create.ts
CHANGED
|
@@ -86,7 +86,7 @@ const collectSurfaceFiles = async (
|
|
|
86
86
|
for (const surface of surfaces) {
|
|
87
87
|
const result = await addSurface(surface);
|
|
88
88
|
if (result.isErr()) {
|
|
89
|
-
return
|
|
89
|
+
return result;
|
|
90
90
|
}
|
|
91
91
|
if (result.value.created !== null) {
|
|
92
92
|
created.push(result.value.created);
|
|
@@ -181,7 +181,7 @@ const writeReadme = async (
|
|
|
181
181
|
): Promise<Result<string | null, Error>> => {
|
|
182
182
|
const exists = projectPathExists(dir, 'README.md');
|
|
183
183
|
if (exists.isErr()) {
|
|
184
|
-
return
|
|
184
|
+
return exists;
|
|
185
185
|
}
|
|
186
186
|
if (exists.value) {
|
|
187
187
|
return Result.ok(null);
|
|
@@ -225,7 +225,7 @@ export const createTrail = trail('create', {
|
|
|
225
225
|
)
|
|
226
226
|
);
|
|
227
227
|
if (surfaceFiles.isErr()) {
|
|
228
|
-
return
|
|
228
|
+
return surfaceFiles;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const verifyFiles = await collectVerifyFiles(input.verify, () =>
|
|
@@ -235,12 +235,12 @@ export const createTrail = trail('create', {
|
|
|
235
235
|
)
|
|
236
236
|
);
|
|
237
237
|
if (verifyFiles.isErr()) {
|
|
238
|
-
return
|
|
238
|
+
return verifyFiles;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
const readmeFile = await writeReadme(input, scaffolded.value.dir);
|
|
242
242
|
if (readmeFile.isErr()) {
|
|
243
|
-
return
|
|
243
|
+
return readmeFile;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
return Result.ok({
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
openReadTrailsDb,
|
|
6
6
|
openWriteTrailsDb,
|
|
7
7
|
deriveTrailsDbPath,
|
|
8
|
-
deriveTrailsDir,
|
|
9
8
|
} from '@ontrails/core';
|
|
10
9
|
import {
|
|
11
10
|
countPinnedSnapshots,
|
|
@@ -21,7 +20,10 @@ import {
|
|
|
21
20
|
previewTraceCleanup,
|
|
22
21
|
} from '@ontrails/tracing';
|
|
23
22
|
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
removeFileIfPresent,
|
|
25
|
+
removeRootRelativeFileIfPresent,
|
|
26
|
+
} from '../local-state-io.js';
|
|
25
27
|
|
|
26
28
|
import { requireTrailRootDir } from './root-dir.js';
|
|
27
29
|
|
|
@@ -31,9 +33,17 @@ const deriveRootDir = (cwd?: string): string => requireTrailRootDir(cwd);
|
|
|
31
33
|
|
|
32
34
|
const removeResetFileIfPresent = (
|
|
33
35
|
rootDir: string,
|
|
34
|
-
|
|
36
|
+
resetPath: string
|
|
35
37
|
): boolean => {
|
|
36
|
-
|
|
38
|
+
if (isAbsolute(resetPath)) {
|
|
39
|
+
const removed = removeFileIfPresent(resetPath);
|
|
40
|
+
if (removed.isErr()) {
|
|
41
|
+
throw removed.error;
|
|
42
|
+
}
|
|
43
|
+
return removed.value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const removed = removeRootRelativeFileIfPresent(rootDir, resetPath);
|
|
37
47
|
if (removed.isErr()) {
|
|
38
48
|
throw removed.error;
|
|
39
49
|
}
|
|
@@ -139,7 +149,7 @@ const buildDbStats = (
|
|
|
139
149
|
): DevStatsReport['db'] => ({
|
|
140
150
|
exists,
|
|
141
151
|
fileSizeBytes: exists ? statSync(dbPath).size : 0,
|
|
142
|
-
path:
|
|
152
|
+
path: dbPath,
|
|
143
153
|
});
|
|
144
154
|
|
|
145
155
|
const emptyDevStats = (
|
|
@@ -184,8 +194,7 @@ const liveDevStats = (
|
|
|
184
194
|
const deriveDevStatsContext = (options?: DevRetentionOptions) => {
|
|
185
195
|
const rootDir = deriveRootDir(options?.rootDir);
|
|
186
196
|
const dbPath = deriveTrailsDbPath({ rootDir });
|
|
187
|
-
const
|
|
188
|
-
const lockPath = join(trailsDir, 'trails.lock');
|
|
197
|
+
const lockPath = join(rootDir, 'trails.lock');
|
|
189
198
|
return {
|
|
190
199
|
dbExists: existsSync(dbPath),
|
|
191
200
|
dbPath,
|
|
@@ -256,24 +265,35 @@ const buildCleanReport = (
|
|
|
256
265
|
};
|
|
257
266
|
};
|
|
258
267
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
268
|
+
const dbSidecarPaths = (basePath: string): readonly string[] => [
|
|
269
|
+
basePath,
|
|
270
|
+
`${basePath}-shm`,
|
|
271
|
+
`${basePath}-wal`,
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const legacyRepoPath = (...segments: readonly string[]) => segments.join('/');
|
|
275
|
+
const legacyDbName = (...segments: readonly string[]) => segments.join('.');
|
|
276
|
+
|
|
277
|
+
const legacyResetFiles = [
|
|
278
|
+
// Legacy paths (pre-state migration) cleaned for one cycle so upgrading
|
|
279
|
+
// workspaces do not leave stale DB sidecars at old repo-local locations.
|
|
280
|
+
...dbSidecarPaths(legacyRepoPath('.trails', 'state', 'trails.db')),
|
|
281
|
+
...dbSidecarPaths(legacyRepoPath('.trails', legacyDbName('trails', 'db'))),
|
|
282
|
+
...dbSidecarPaths(legacyRepoPath('.trails', 'dev', 'tracing.db')),
|
|
271
283
|
] as const;
|
|
272
284
|
|
|
273
|
-
const
|
|
274
|
-
rootDir
|
|
275
|
-
|
|
276
|
-
|
|
285
|
+
const currentResetFiles = (rootDir: string): readonly string[] => {
|
|
286
|
+
const dbPath = deriveTrailsDbPath({ rootDir });
|
|
287
|
+
return dbSidecarPaths(dbPath);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const resetFileExists = (rootDir: string, resetPath: string): boolean =>
|
|
291
|
+
existsSync(isAbsolute(resetPath) ? resetPath : join(rootDir, resetPath));
|
|
292
|
+
|
|
293
|
+
const presentResetFiles = (rootDir: string): readonly string[] =>
|
|
294
|
+
[...currentResetFiles(rootDir), ...legacyResetFiles].filter((resetPath) =>
|
|
295
|
+
resetFileExists(rootDir, resetPath)
|
|
296
|
+
);
|
|
277
297
|
|
|
278
298
|
export const buildDevStats = (
|
|
279
299
|
options?: DevRetentionOptions
|
package/src/trails/doctor.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
deriveTrailsDir,
|
|
3
|
+
InternalError,
|
|
4
|
+
isTrailsError,
|
|
5
|
+
Result,
|
|
6
|
+
trail,
|
|
7
|
+
} from '@ontrails/core';
|
|
2
8
|
import { readTopoGraph } from '@ontrails/topographer';
|
|
3
9
|
import { z } from 'zod';
|
|
4
10
|
|
|
@@ -18,11 +24,26 @@ const readDoctorForceGraph = async (
|
|
|
18
24
|
}
|
|
19
25
|
};
|
|
20
26
|
|
|
27
|
+
const toDoctorSummaryError = (error: unknown): InternalError =>
|
|
28
|
+
error instanceof Error
|
|
29
|
+
? new InternalError('Unable to derive doctor summary', { cause: error })
|
|
30
|
+
: new InternalError(`Unable to derive doctor summary: ${String(error)}`);
|
|
31
|
+
|
|
21
32
|
export const doctorTrail = trail('doctor', {
|
|
22
33
|
blaze: async (input, ctx) =>
|
|
23
34
|
withLifecycleApp(input, ctx.cwd, async (app, rootDir) => {
|
|
24
35
|
const forceGraph = await readDoctorForceGraph(rootDir);
|
|
25
|
-
|
|
36
|
+
try {
|
|
37
|
+
return Result.ok(deriveDoctorSummary(app, { forceGraph }));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// deriveTopoGraph throws ValidationError on invalid topos; doctor must
|
|
40
|
+
// report that diagnosis instead of letting the pipeline redact it to a
|
|
41
|
+
// generic internal error.
|
|
42
|
+
if (isTrailsError(error)) {
|
|
43
|
+
return Result.err(error);
|
|
44
|
+
}
|
|
45
|
+
return Result.err(toDoctorSummaryError(error));
|
|
46
|
+
}
|
|
26
47
|
}),
|
|
27
48
|
description: 'Diagnose trail versioning lifecycle state',
|
|
28
49
|
input: z.object({
|
|
@@ -571,7 +571,7 @@ const planRelativeImports = (
|
|
|
571
571
|
operations
|
|
572
572
|
);
|
|
573
573
|
if (changed.isErr()) {
|
|
574
|
-
return
|
|
574
|
+
return changed;
|
|
575
575
|
}
|
|
576
576
|
if (changed.value) {
|
|
577
577
|
updatedSourceFiles.add(filePath);
|
|
@@ -593,7 +593,7 @@ const rewritePromotionState = async (
|
|
|
593
593
|
const initialFiles = collectTsFiles(rootDir);
|
|
594
594
|
const sourcesResult = await readSourceFiles(initialFiles);
|
|
595
595
|
if (sourcesResult.isErr()) {
|
|
596
|
-
return
|
|
596
|
+
return sourcesResult;
|
|
597
597
|
}
|
|
598
598
|
const sources = sourcesResult.value;
|
|
599
599
|
const operations: ProjectWriteOperation[] = [];
|
|
@@ -608,14 +608,14 @@ const rewritePromotionState = async (
|
|
|
608
608
|
operations
|
|
609
609
|
);
|
|
610
610
|
if (rewritten.isErr()) {
|
|
611
|
-
return
|
|
611
|
+
return rewritten;
|
|
612
612
|
}
|
|
613
613
|
|
|
614
614
|
const renamesResult = input.renameFiles
|
|
615
615
|
? collectFileRenames(initialFiles, sources)
|
|
616
616
|
: Result.ok([] as FileRename[]);
|
|
617
617
|
if (renamesResult.isErr()) {
|
|
618
|
-
return
|
|
618
|
+
return renamesResult;
|
|
619
619
|
}
|
|
620
620
|
|
|
621
621
|
const valid = validateRenameTargets(renamesResult.value);
|
|
@@ -637,14 +637,14 @@ const rewritePromotionState = async (
|
|
|
637
637
|
operations
|
|
638
638
|
);
|
|
639
639
|
if (importUpdates.isErr()) {
|
|
640
|
-
return
|
|
640
|
+
return importUpdates;
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
const plannedOperations = input.dryRun
|
|
644
644
|
? planProjectOperations(rootDir, operations)
|
|
645
645
|
: await applyProjectOperations(rootDir, operations);
|
|
646
646
|
if (plannedOperations.isErr()) {
|
|
647
|
-
return
|
|
647
|
+
return plannedOperations;
|
|
648
648
|
}
|
|
649
649
|
return Result.ok({
|
|
650
650
|
plannedOperations: plannedOperations.value,
|
|
@@ -837,7 +837,7 @@ const promoteDraftState = async (
|
|
|
837
837
|
|
|
838
838
|
const rewriteResult = await rewritePromotionState(rootDirResult.value, input);
|
|
839
839
|
if (rewriteResult.isErr()) {
|
|
840
|
-
return
|
|
840
|
+
return rewriteResult;
|
|
841
841
|
}
|
|
842
842
|
|
|
843
843
|
const { renames, updatedSourceFiles } = rewriteResult.value;
|