@ontrails/trails 1.0.0-beta.15 → 1.0.0-beta.16
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 +197 -2
- package/README.md +27 -0
- package/package.json +19 -8
- package/src/app.ts +15 -5
- package/src/cli.ts +303 -10
- package/src/completions.ts +240 -0
- package/src/load-app-mirror.ts +160 -0
- package/src/local-state-io.ts +153 -0
- package/src/project-writes.ts +320 -0
- package/src/run-collision.ts +125 -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-trace.ts +273 -0
- package/src/run-warden.ts +39 -0
- package/src/run-watch.ts +432 -0
- package/src/scaffold-versions.generated.ts +12 -0
- package/src/trails/add-surface.ts +45 -23
- package/src/trails/add-trail.ts +27 -17
- package/src/trails/add-verify.ts +57 -17
- package/src/trails/completions-complete.ts +165 -0
- package/src/trails/completions.ts +47 -0
- package/src/trails/create-scaffold.ts +86 -33
- package/src/trails/create.ts +11 -3
- package/src/trails/dev-clean.ts +6 -1
- package/src/trails/dev-reset.ts +6 -1
- package/src/trails/dev-stats.ts +6 -1
- package/src/trails/dev-support.ts +29 -17
- package/src/trails/draft-promote.ts +289 -80
- package/src/trails/guide.ts +54 -34
- package/src/trails/load-app.ts +251 -56
- package/src/trails/root-dir.ts +21 -0
- package/src/trails/run-example.ts +482 -0
- package/src/trails/run-examples.ts +141 -0
- package/src/trails/run.ts +403 -0
- package/src/trails/survey.ts +506 -200
- package/src/trails/topo-activation.ts +385 -0
- package/src/trails/topo-compile.ts +55 -0
- package/src/trails/topo-history.ts +6 -1
- package/src/trails/topo-output-schemas.ts +175 -0
- package/src/trails/topo-pin.ts +19 -6
- package/src/trails/topo-read-support.ts +171 -228
- package/src/trails/topo-reports.ts +400 -25
- package/src/trails/topo-store-support.ts +43 -19
- package/src/trails/topo-support.ts +18 -28
- package/src/trails/topo-unpin.ts +6 -1
- package/src/trails/topo-verify.ts +18 -5
- package/src/trails/topo.ts +60 -23
- package/src/trails/warden-guide.ts +121 -0
- package/src/trails/warden.ts +137 -56
- package/src/versions.ts +3 -18
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/__tests__/examples.test.ts +0 -45
- package/dist/bin/trails.d.ts +0 -3
- package/dist/bin/trails.d.ts.map +0 -1
- package/dist/bin/trails.js +0 -4
- package/dist/bin/trails.js.map +0 -1
- package/dist/src/app.d.ts +0 -2
- package/dist/src/app.d.ts.map +0 -1
- package/dist/src/app.js +0 -22
- package/dist/src/app.js.map +0 -1
- package/dist/src/clack.d.ts +0 -9
- package/dist/src/clack.d.ts.map +0 -1
- package/dist/src/clack.js +0 -84
- package/dist/src/clack.js.map +0 -1
- package/dist/src/cli.d.ts +0 -2
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -14
- package/dist/src/cli.js.map +0 -1
- package/dist/src/trails/add-surface.d.ts +0 -13
- package/dist/src/trails/add-surface.d.ts.map +0 -1
- package/dist/src/trails/add-surface.js +0 -110
- package/dist/src/trails/add-surface.js.map +0 -1
- package/dist/src/trails/add-trail.d.ts +0 -12
- package/dist/src/trails/add-trail.d.ts.map +0 -1
- package/dist/src/trails/add-trail.js +0 -104
- package/dist/src/trails/add-trail.js.map +0 -1
- package/dist/src/trails/add-trailhead.d.ts +0 -13
- package/dist/src/trails/add-trailhead.d.ts.map +0 -1
- package/dist/src/trails/add-trailhead.js +0 -88
- package/dist/src/trails/add-trailhead.js.map +0 -1
- package/dist/src/trails/add-verify.d.ts +0 -10
- package/dist/src/trails/add-verify.d.ts.map +0 -1
- package/dist/src/trails/add-verify.js +0 -68
- package/dist/src/trails/add-verify.js.map +0 -1
- package/dist/src/trails/create-scaffold.d.ts +0 -15
- package/dist/src/trails/create-scaffold.d.ts.map +0 -1
- package/dist/src/trails/create-scaffold.js +0 -295
- package/dist/src/trails/create-scaffold.js.map +0 -1
- package/dist/src/trails/create.d.ts +0 -18
- package/dist/src/trails/create.d.ts.map +0 -1
- package/dist/src/trails/create.js +0 -126
- package/dist/src/trails/create.js.map +0 -1
- package/dist/src/trails/dev-clean.d.ts +0 -9
- package/dist/src/trails/dev-clean.d.ts.map +0 -1
- package/dist/src/trails/dev-clean.js +0 -66
- package/dist/src/trails/dev-clean.js.map +0 -1
- package/dist/src/trails/dev-reset.d.ts +0 -6
- package/dist/src/trails/dev-reset.d.ts.map +0 -1
- package/dist/src/trails/dev-reset.js +0 -39
- package/dist/src/trails/dev-reset.js.map +0 -1
- package/dist/src/trails/dev-stats.d.ts +0 -7
- package/dist/src/trails/dev-stats.d.ts.map +0 -1
- package/dist/src/trails/dev-stats.js +0 -61
- package/dist/src/trails/dev-stats.js.map +0 -1
- package/dist/src/trails/dev-support.d.ts +0 -64
- package/dist/src/trails/dev-support.d.ts.map +0 -1
- package/dist/src/trails/dev-support.js +0 -181
- package/dist/src/trails/dev-support.js.map +0 -1
- package/dist/src/trails/draft-promote.d.ts +0 -18
- package/dist/src/trails/draft-promote.d.ts.map +0 -1
- package/dist/src/trails/draft-promote.js +0 -400
- package/dist/src/trails/draft-promote.js.map +0 -1
- package/dist/src/trails/guide.d.ts +0 -21
- package/dist/src/trails/guide.d.ts.map +0 -1
- package/dist/src/trails/guide.js +0 -61
- package/dist/src/trails/guide.js.map +0 -1
- package/dist/src/trails/load-app.d.ts +0 -12
- package/dist/src/trails/load-app.d.ts.map +0 -1
- package/dist/src/trails/load-app.js +0 -415
- package/dist/src/trails/load-app.js.map +0 -1
- package/dist/src/trails/project.d.ts +0 -8
- package/dist/src/trails/project.d.ts.map +0 -1
- package/dist/src/trails/project.js +0 -54
- package/dist/src/trails/project.js.map +0 -1
- package/dist/src/trails/survey.d.ts +0 -18
- package/dist/src/trails/survey.d.ts.map +0 -1
- package/dist/src/trails/survey.js +0 -234
- package/dist/src/trails/survey.js.map +0 -1
- package/dist/src/trails/topo-constants.d.ts +0 -3
- package/dist/src/trails/topo-constants.d.ts.map +0 -1
- package/dist/src/trails/topo-constants.js +0 -3
- package/dist/src/trails/topo-constants.js.map +0 -1
- package/dist/src/trails/topo-export.d.ts +0 -19
- package/dist/src/trails/topo-export.d.ts.map +0 -1
- package/dist/src/trails/topo-export.js +0 -31
- package/dist/src/trails/topo-export.js.map +0 -1
- package/dist/src/trails/topo-history.d.ts +0 -20
- package/dist/src/trails/topo-history.d.ts.map +0 -1
- package/dist/src/trails/topo-history.js +0 -32
- package/dist/src/trails/topo-history.js.map +0 -1
- package/dist/src/trails/topo-pin.d.ts +0 -17
- package/dist/src/trails/topo-pin.d.ts.map +0 -1
- package/dist/src/trails/topo-pin.js +0 -31
- package/dist/src/trails/topo-pin.js.map +0 -1
- package/dist/src/trails/topo-read-support.d.ts +0 -58
- package/dist/src/trails/topo-read-support.d.ts.map +0 -1
- package/dist/src/trails/topo-read-support.js +0 -167
- package/dist/src/trails/topo-read-support.js.map +0 -1
- package/dist/src/trails/topo-reports.d.ts +0 -54
- package/dist/src/trails/topo-reports.d.ts.map +0 -1
- package/dist/src/trails/topo-reports.js +0 -128
- package/dist/src/trails/topo-reports.js.map +0 -1
- package/dist/src/trails/topo-show.d.ts +0 -23
- package/dist/src/trails/topo-show.d.ts.map +0 -1
- package/dist/src/trails/topo-show.js +0 -49
- package/dist/src/trails/topo-show.js.map +0 -1
- package/dist/src/trails/topo-store-support.d.ts +0 -13
- package/dist/src/trails/topo-store-support.d.ts.map +0 -1
- package/dist/src/trails/topo-store-support.js +0 -55
- package/dist/src/trails/topo-store-support.js.map +0 -1
- package/dist/src/trails/topo-support.d.ts +0 -76
- package/dist/src/trails/topo-support.d.ts.map +0 -1
- package/dist/src/trails/topo-support.js +0 -132
- package/dist/src/trails/topo-support.js.map +0 -1
- package/dist/src/trails/topo-unpin.d.ts +0 -20
- package/dist/src/trails/topo-unpin.d.ts.map +0 -1
- package/dist/src/trails/topo-unpin.js +0 -44
- package/dist/src/trails/topo-unpin.js.map +0 -1
- package/dist/src/trails/topo-verify.d.ts +0 -5
- package/dist/src/trails/topo-verify.d.ts.map +0 -1
- package/dist/src/trails/topo-verify.js +0 -24
- package/dist/src/trails/topo-verify.js.map +0 -1
- package/dist/src/trails/topo.d.ts +0 -5
- package/dist/src/trails/topo.d.ts.map +0 -1
- package/dist/src/trails/topo.js +0 -63
- package/dist/src/trails/topo.js.map +0 -1
- package/dist/src/trails/warden.d.ts +0 -20
- package/dist/src/trails/warden.d.ts.map +0 -1
- package/dist/src/trails/warden.js +0 -98
- package/dist/src/trails/warden.js.map +0 -1
- package/dist/src/versions.d.ts +0 -12
- package/dist/src/versions.d.ts.map +0 -1
- package/dist/src/versions.js +0 -23
- package/dist/src/versions.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/__tests__/add-trail.test.ts +0 -97
- package/src/__tests__/create.test.ts +0 -415
- package/src/__tests__/draft-promote.test.ts +0 -144
- package/src/__tests__/guide.test.ts +0 -96
- package/src/__tests__/load-app.test.ts +0 -419
- package/src/__tests__/survey.test.ts +0 -377
- package/src/__tests__/topo-dev.test.ts +0 -426
- package/src/__tests__/warden.test.ts +0 -74
- package/src/trails/topo-export.ts +0 -35
- package/src/trails/topo-show.ts +0 -54
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
package/src/trails/add-verify.ts
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
* `add.verify` trail -- Add testing + warden setup to a project.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync
|
|
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
|
+
resolveProjectDir,
|
|
14
|
+
resolveProjectPath,
|
|
15
|
+
writeProjectFile,
|
|
16
|
+
} from '../project-writes.js';
|
|
11
17
|
import {
|
|
12
18
|
ontrailsPackageRange,
|
|
13
19
|
scaffoldDependencyVersions,
|
|
@@ -45,14 +51,24 @@ const patchVerifyDeps = (pkg: Record<string, unknown>): void => {
|
|
|
45
51
|
/** Update package.json in the target project with verify dependencies. */
|
|
46
52
|
const updatePackageJsonForVerify = async (
|
|
47
53
|
projectDir: string
|
|
48
|
-
): Promise<void
|
|
49
|
-
const
|
|
54
|
+
): Promise<Result<void, Error>> => {
|
|
55
|
+
const pkgPathResult = resolveProjectPath(projectDir, 'package.json');
|
|
56
|
+
if (pkgPathResult.isErr()) {
|
|
57
|
+
return Result.err(pkgPathResult.error);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pkgPath = pkgPathResult.value;
|
|
50
61
|
if (!existsSync(pkgPath)) {
|
|
51
|
-
return;
|
|
62
|
+
return Result.ok();
|
|
52
63
|
}
|
|
53
64
|
const pkg = (await Bun.file(pkgPath).json()) as Record<string, unknown>;
|
|
54
65
|
patchVerifyDeps(pkg);
|
|
55
|
-
|
|
66
|
+
const written = await writeProjectFile(
|
|
67
|
+
projectDir,
|
|
68
|
+
'package.json',
|
|
69
|
+
`${JSON.stringify(pkg, null, 2)}\n`
|
|
70
|
+
);
|
|
71
|
+
return written.isErr() ? Result.err(written.error) : Result.ok();
|
|
56
72
|
};
|
|
57
73
|
|
|
58
74
|
// ---------------------------------------------------------------------------
|
|
@@ -61,32 +77,56 @@ const updatePackageJsonForVerify = async (
|
|
|
61
77
|
|
|
62
78
|
export const addVerify = trail('add.verify', {
|
|
63
79
|
blaze: async (input) => {
|
|
64
|
-
const
|
|
80
|
+
const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
|
|
81
|
+
if (projectDirResult.isErr()) {
|
|
82
|
+
return Result.err(projectDirResult.error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const projectDir = projectDirResult.value;
|
|
65
86
|
const files: string[] = [];
|
|
66
87
|
|
|
67
88
|
const writeFile = async (
|
|
68
89
|
relativePath: string,
|
|
69
90
|
content: string
|
|
70
|
-
): Promise<void
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
): Promise<Result<void, Error>> => {
|
|
92
|
+
const written = await writeProjectFile(projectDir, relativePath, content);
|
|
93
|
+
if (written.isErr()) {
|
|
94
|
+
return Result.err(written.error);
|
|
95
|
+
}
|
|
96
|
+
files.push(written.value);
|
|
97
|
+
return Result.ok();
|
|
75
98
|
};
|
|
76
99
|
|
|
77
|
-
await writeFile(
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
const testFile = await writeFile(
|
|
101
|
+
'__tests__/examples.test.ts',
|
|
102
|
+
generateTestFile()
|
|
103
|
+
);
|
|
104
|
+
if (testFile.isErr()) {
|
|
105
|
+
return Result.err(testFile.error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const lefthookFile = await writeFile('lefthook.yml', generateLefthookYml());
|
|
109
|
+
if (lefthookFile.isErr()) {
|
|
110
|
+
return Result.err(lefthookFile.error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const packageResult = await updatePackageJsonForVerify(projectDir);
|
|
114
|
+
if (packageResult.isErr()) {
|
|
115
|
+
return Result.err(packageResult.error);
|
|
116
|
+
}
|
|
80
117
|
|
|
81
118
|
return Result.ok({ created: files });
|
|
82
119
|
},
|
|
83
120
|
description: 'Add testing and warden verification',
|
|
84
121
|
input: z.object({
|
|
85
122
|
dir: z.string().optional().describe('Parent directory'),
|
|
86
|
-
name: z
|
|
123
|
+
name: z
|
|
124
|
+
.string()
|
|
125
|
+
.regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
|
|
126
|
+
.describe('Project name'),
|
|
87
127
|
}),
|
|
88
|
-
meta: { internal: true },
|
|
89
128
|
output: z.object({
|
|
90
129
|
created: z.array(z.string()),
|
|
91
130
|
}),
|
|
131
|
+
visibility: 'internal',
|
|
92
132
|
});
|
|
@@ -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
|
+
blaze: async (input, ctx) => {
|
|
122
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
123
|
+
if (rootDirResult.isErr()) {
|
|
124
|
+
return Result.err(rootDirResult.error);
|
|
125
|
+
}
|
|
126
|
+
const rootDir = rootDirResult.value;
|
|
127
|
+
|
|
128
|
+
const [subcommand] = input.args;
|
|
129
|
+
if (subcommand === undefined) {
|
|
130
|
+
return Result.ok(EMPTY_SUGGESTIONS);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handler = SUBCOMMAND_HANDLERS[subcommand];
|
|
134
|
+
if (handler === undefined) {
|
|
135
|
+
return Result.ok(EMPTY_SUGGESTIONS);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const suggestions = await handler({ args: input.args, rootDir });
|
|
139
|
+
return Result.ok(renderSuggestions(suggestions));
|
|
140
|
+
},
|
|
141
|
+
description:
|
|
142
|
+
'Internal: emit dynamic completion suggestions for the current partial argv. Invoked by the static shell completion script at tab-press time.',
|
|
143
|
+
examples: [
|
|
144
|
+
{
|
|
145
|
+
description: 'Empty argv yields no suggestions',
|
|
146
|
+
input: { args: [] },
|
|
147
|
+
name: 'Empty args',
|
|
148
|
+
},
|
|
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
|
+
blaze: async (input) =>
|
|
20
|
+
renderCompletionScript(input.shell, COMPLETIONS_BIN_NAME),
|
|
21
|
+
description:
|
|
22
|
+
'Print a shell completion script for the trails CLI; pipe into your shell rc to register tab-completion',
|
|
23
|
+
examples: [
|
|
24
|
+
{
|
|
25
|
+
description: 'Render a bash completion script',
|
|
26
|
+
input: { shell: 'bash' },
|
|
27
|
+
name: 'Render bash completion',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
description: 'Render a zsh completion script',
|
|
31
|
+
input: { shell: 'zsh' },
|
|
32
|
+
name: 'Render zsh completion',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
description: 'Render a fish completion script',
|
|
36
|
+
input: { shell: 'fish' },
|
|
37
|
+
name: 'Render fish completion',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
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
|
+
});
|
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
* Generates package.json, tsconfig, app.ts, starter trails, and .trails/ directory.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
9
8
|
|
|
10
|
-
import { Result, trail } from '@ontrails/core';
|
|
9
|
+
import { Result, trail, WORKSPACE_GITIGNORE_CONTENT } from '@ontrails/core';
|
|
11
10
|
import { z } from 'zod';
|
|
12
11
|
|
|
12
|
+
import {
|
|
13
|
+
applyProjectOperations,
|
|
14
|
+
planProjectOperations,
|
|
15
|
+
PROJECT_NAME_MESSAGE,
|
|
16
|
+
PROJECT_NAME_PATTERN,
|
|
17
|
+
resolveProjectDir,
|
|
18
|
+
} from '../project-writes.js';
|
|
19
|
+
import type {
|
|
20
|
+
PlannedProjectOperation,
|
|
21
|
+
ProjectWriteOperation,
|
|
22
|
+
} from '../project-writes.js';
|
|
13
23
|
import {
|
|
14
24
|
ontrailsPackageRange,
|
|
15
25
|
scaffoldDependencyVersions,
|
|
@@ -24,7 +34,9 @@ type Starter = 'empty' | 'entity' | 'hello';
|
|
|
24
34
|
interface ScaffoldResult {
|
|
25
35
|
readonly created: string[];
|
|
26
36
|
readonly dir: string;
|
|
37
|
+
readonly dryRun: boolean;
|
|
27
38
|
readonly name: string;
|
|
39
|
+
readonly plannedOperations: PlannedProjectOperation[];
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
// ---------------------------------------------------------------------------
|
|
@@ -44,6 +56,7 @@ const generatePackageJson = (name: string): string => {
|
|
|
44
56
|
devDependencies: Object.fromEntries(
|
|
45
57
|
Object.entries({
|
|
46
58
|
'@types/bun': scaffoldDependencyVersions.bunTypes,
|
|
59
|
+
oxfmt: scaffoldDependencyVersions.oxfmt,
|
|
47
60
|
oxlint: scaffoldDependencyVersions.oxlint,
|
|
48
61
|
typescript: scaffoldDependencyVersions.typescript,
|
|
49
62
|
ultracite: scaffoldDependencyVersions.ultracite,
|
|
@@ -52,6 +65,8 @@ const generatePackageJson = (name: string): string => {
|
|
|
52
65
|
name,
|
|
53
66
|
scripts: {
|
|
54
67
|
build: 'tsc -b',
|
|
68
|
+
'format:check': 'bunx ultracite check .',
|
|
69
|
+
'format:fix': 'bunx ultracite fix .',
|
|
55
70
|
lint: 'oxlint ./src',
|
|
56
71
|
test: 'bun test',
|
|
57
72
|
typecheck: 'tsc --noEmit',
|
|
@@ -86,16 +101,19 @@ const TSCONFIG_CONTENT = JSON.stringify(
|
|
|
86
101
|
const GITIGNORE_CONTENT = `node_modules/
|
|
87
102
|
dist/
|
|
88
103
|
*.tsbuildinfo
|
|
89
|
-
.trails/
|
|
104
|
+
.trails/cache/
|
|
105
|
+
.trails/state/
|
|
106
|
+
.trails/config.local.js
|
|
107
|
+
.trails/config.local.ts
|
|
90
108
|
`;
|
|
91
109
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
const OXLINT_CONFIG_CONTENT = `import { defineConfig } from 'oxlint';
|
|
111
|
+
import ultracite from 'ultracite/oxlint/core';
|
|
112
|
+
|
|
113
|
+
export default defineConfig({
|
|
114
|
+
extends: [ultracite],
|
|
115
|
+
});
|
|
116
|
+
`;
|
|
99
117
|
|
|
100
118
|
const OXFMTRC_CONTENT = `{
|
|
101
119
|
// ultracite defaults
|
|
@@ -273,8 +291,11 @@ const starterImports: Record<
|
|
|
273
291
|
|
|
274
292
|
const generateAppTs = (name: string, starter: Starter): string => {
|
|
275
293
|
const { imports, modules } = starterImports[starter];
|
|
294
|
+
const appNameLiteral = JSON.stringify(name);
|
|
276
295
|
const topoArgs =
|
|
277
|
-
modules.length > 0
|
|
296
|
+
modules.length > 0
|
|
297
|
+
? `${appNameLiteral}, ${modules.join(', ')}`
|
|
298
|
+
: appNameLiteral;
|
|
278
299
|
|
|
279
300
|
return [
|
|
280
301
|
"import { topo } from '@ontrails/core';",
|
|
@@ -309,25 +330,21 @@ const collectScaffoldFiles = (
|
|
|
309
330
|
['package.json', generatePackageJson(name)],
|
|
310
331
|
['tsconfig.json', TSCONFIG_CONTENT],
|
|
311
332
|
['.gitignore', GITIGNORE_CONTENT],
|
|
312
|
-
['.
|
|
333
|
+
['oxlint.config.ts', OXLINT_CONFIG_CONTENT],
|
|
313
334
|
['.oxfmtrc.jsonc', OXFMTRC_CONTENT],
|
|
335
|
+
['.trails/.gitignore', WORKSPACE_GITIGNORE_CONTENT],
|
|
314
336
|
['src/app.ts', generateAppTs(name, starter)],
|
|
315
337
|
...starterFileGenerators[starter](),
|
|
316
338
|
]);
|
|
317
339
|
|
|
318
|
-
const
|
|
319
|
-
projectDir: string,
|
|
340
|
+
const collectScaffoldOperations = (
|
|
320
341
|
fileMap: Map<string, string>
|
|
321
|
-
):
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
files.push(relativePath);
|
|
328
|
-
}
|
|
329
|
-
return files;
|
|
330
|
-
};
|
|
342
|
+
): ProjectWriteOperation[] =>
|
|
343
|
+
[...fileMap].map(([path, content]) => ({
|
|
344
|
+
content,
|
|
345
|
+
kind: 'write' as const,
|
|
346
|
+
path,
|
|
347
|
+
}));
|
|
331
348
|
|
|
332
349
|
// ---------------------------------------------------------------------------
|
|
333
350
|
// Trail definition
|
|
@@ -335,31 +352,67 @@ const writeScaffoldFiles = async (
|
|
|
335
352
|
|
|
336
353
|
export const createScaffold = trail('create.scaffold', {
|
|
337
354
|
blaze: async (input) => {
|
|
338
|
-
const
|
|
355
|
+
const projectDirResult = resolveProjectDir(input.dir ?? '.', input.name);
|
|
356
|
+
if (projectDirResult.isErr()) {
|
|
357
|
+
return Result.err(projectDirResult.error);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const projectDir = projectDirResult.value;
|
|
339
361
|
const starter = (input.starter ?? 'hello') as Starter;
|
|
362
|
+
const dryRun = input.dryRun === true;
|
|
340
363
|
const fileMap = collectScaffoldFiles(input.name, starter);
|
|
341
|
-
const
|
|
342
|
-
|
|
364
|
+
const operations = collectScaffoldOperations(fileMap);
|
|
365
|
+
const plannedOperations = dryRun
|
|
366
|
+
? planProjectOperations(projectDir, operations)
|
|
367
|
+
: await applyProjectOperations(projectDir, operations);
|
|
368
|
+
if (plannedOperations.isErr()) {
|
|
369
|
+
return Result.err(plannedOperations.error);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const created = dryRun ? [] : [...fileMap.keys()];
|
|
343
373
|
|
|
344
374
|
return Result.ok({
|
|
345
|
-
created
|
|
346
|
-
dir: projectDir,
|
|
375
|
+
created,
|
|
376
|
+
dir: resolve(projectDir),
|
|
377
|
+
dryRun,
|
|
347
378
|
name: input.name,
|
|
379
|
+
plannedOperations: plannedOperations.value,
|
|
348
380
|
} satisfies ScaffoldResult);
|
|
349
381
|
},
|
|
350
382
|
description: 'Scaffold a new Trails project',
|
|
351
383
|
input: z.object({
|
|
352
384
|
dir: z.string().optional().describe('Parent directory'),
|
|
353
|
-
|
|
385
|
+
dryRun: z
|
|
386
|
+
.boolean()
|
|
387
|
+
.default(false)
|
|
388
|
+
.describe('Plan scaffold writes without touching the project directory'),
|
|
389
|
+
name: z
|
|
390
|
+
.string()
|
|
391
|
+
.regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
|
|
392
|
+
.describe('Project name'),
|
|
354
393
|
starter: z
|
|
355
394
|
.enum(['hello', 'entity', 'empty'])
|
|
356
395
|
.default('hello')
|
|
357
396
|
.describe('Starter trail'),
|
|
358
397
|
}),
|
|
359
|
-
meta: { internal: true },
|
|
360
398
|
output: z.object({
|
|
361
|
-
created: z
|
|
399
|
+
created: z
|
|
400
|
+
.array(z.string())
|
|
401
|
+
.describe('Project-relative paths of files written (empty in dry-run)'),
|
|
362
402
|
dir: z.string(),
|
|
403
|
+
dryRun: z.boolean(),
|
|
363
404
|
name: z.string(),
|
|
405
|
+
plannedOperations: z.array(
|
|
406
|
+
z.discriminatedUnion('kind', [
|
|
407
|
+
z.object({ kind: z.literal('mkdir'), path: z.string() }),
|
|
408
|
+
z.object({
|
|
409
|
+
from: z.string(),
|
|
410
|
+
kind: z.literal('rename'),
|
|
411
|
+
to: z.string(),
|
|
412
|
+
}),
|
|
413
|
+
z.object({ kind: z.literal('write'), path: z.string() }),
|
|
414
|
+
])
|
|
415
|
+
),
|
|
364
416
|
}),
|
|
417
|
+
visibility: 'internal',
|
|
365
418
|
});
|
package/src/trails/create.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* via ctx.cross.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Result, trail } from '@ontrails/core';
|
|
8
|
+
import { InternalError, Result, trail } from '@ontrails/core';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
PROJECT_NAME_MESSAGE,
|
|
13
|
+
PROJECT_NAME_PATTERN,
|
|
14
|
+
} from '../project-writes.js';
|
|
15
|
+
|
|
11
16
|
// ---------------------------------------------------------------------------
|
|
12
17
|
// Helpers
|
|
13
18
|
// ---------------------------------------------------------------------------
|
|
@@ -106,7 +111,7 @@ const collectCreatedFiles = (
|
|
|
106
111
|
export const createRoute = trail('create', {
|
|
107
112
|
blaze: async (input: CreateInput, ctx) => {
|
|
108
113
|
if (!ctx.cross) {
|
|
109
|
-
return Result.err(new
|
|
114
|
+
return Result.err(new InternalError('create route requires ctx.cross'));
|
|
110
115
|
}
|
|
111
116
|
const { cross } = ctx;
|
|
112
117
|
|
|
@@ -189,7 +194,10 @@ export const createRoute = trail('create', {
|
|
|
189
194
|
},
|
|
190
195
|
input: z.object({
|
|
191
196
|
dir: z.string().optional().describe('Parent directory'),
|
|
192
|
-
name: z
|
|
197
|
+
name: z
|
|
198
|
+
.string()
|
|
199
|
+
.regex(PROJECT_NAME_PATTERN, PROJECT_NAME_MESSAGE)
|
|
200
|
+
.describe('Project name'),
|
|
193
201
|
starter: z
|
|
194
202
|
.enum(['hello', 'entity', 'empty'])
|
|
195
203
|
.default('hello')
|
package/src/trails/dev-clean.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cleanDevState,
|
|
6
6
|
DEFAULT_TOPO_SNAPSHOT_RETENTION,
|
|
7
7
|
} from './dev-support.js';
|
|
8
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
8
9
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
9
10
|
|
|
10
11
|
export const devCleanTrail = trail('dev.clean', {
|
|
@@ -17,7 +18,11 @@ export const devCleanTrail = trail('dev.clean', {
|
|
|
17
18
|
);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
22
|
+
if (rootDirResult.isErr()) {
|
|
23
|
+
return Result.err(rootDirResult.error);
|
|
24
|
+
}
|
|
25
|
+
const rootDir = rootDirResult.value;
|
|
21
26
|
return Result.ok(
|
|
22
27
|
cleanDevState({
|
|
23
28
|
dryRun: input.dryRun,
|
package/src/trails/dev-reset.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Result, ValidationError, trail } from '@ontrails/core';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import { resetDevState } from './dev-support.js';
|
|
5
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
5
6
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
6
7
|
|
|
7
8
|
export const devResetTrail = trail('dev.reset', {
|
|
@@ -14,7 +15,11 @@ export const devResetTrail = trail('dev.reset', {
|
|
|
14
15
|
);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
19
|
+
if (rootDirResult.isErr()) {
|
|
20
|
+
return Result.err(rootDirResult.error);
|
|
21
|
+
}
|
|
22
|
+
const rootDir = rootDirResult.value;
|
|
18
23
|
return Result.ok(resetDevState({ dryRun: input.dryRun, rootDir }));
|
|
19
24
|
},
|
|
20
25
|
description: 'Remove local Trails database artifacts',
|
package/src/trails/dev-stats.ts
CHANGED
|
@@ -5,11 +5,16 @@ import {
|
|
|
5
5
|
buildDevStats,
|
|
6
6
|
DEFAULT_TOPO_SNAPSHOT_RETENTION,
|
|
7
7
|
} from './dev-support.js';
|
|
8
|
+
import { resolveTrailRootDir } from './root-dir.js';
|
|
8
9
|
import { createIsolatedExampleInput } from './topo-support.js';
|
|
9
10
|
|
|
10
11
|
export const devStatsTrail = trail('dev.stats', {
|
|
11
12
|
blaze: (input, ctx) => {
|
|
12
|
-
const
|
|
13
|
+
const rootDirResult = resolveTrailRootDir(input.rootDir, ctx.cwd);
|
|
14
|
+
if (rootDirResult.isErr()) {
|
|
15
|
+
return Result.err(rootDirResult.error);
|
|
16
|
+
}
|
|
17
|
+
const rootDir = rootDirResult.value;
|
|
13
18
|
return Result.ok(
|
|
14
19
|
buildDevStats({
|
|
15
20
|
maxAge: input.traceAgeMs,
|