@ontrails/trails 1.0.0-beta.13 → 1.0.0-beta.14
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +16 -0
- package/__tests__/examples.test.ts +14 -0
- package/dist/src/app.d.ts.map +1 -1
- package/dist/src/app.js +13 -2
- package/dist/src/app.js.map +1 -1
- package/dist/src/clack.d.ts +1 -1
- package/dist/src/clack.js +1 -1
- package/dist/src/cli.js +2 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/trails/add-trail.js +13 -13
- package/dist/src/trails/add-trail.js.map +1 -1
- package/dist/src/trails/add-trailhead.d.ts +13 -0
- package/dist/src/trails/add-trailhead.d.ts.map +1 -0
- package/dist/src/trails/add-trailhead.js +88 -0
- package/dist/src/trails/add-trailhead.js.map +1 -0
- package/dist/src/trails/add-verify.js +10 -10
- package/dist/src/trails/add-verify.js.map +1 -1
- package/dist/src/trails/create-scaffold.js +26 -26
- package/dist/src/trails/create-scaffold.js.map +1 -1
- package/dist/src/trails/create.d.ts +6 -6
- package/dist/src/trails/create.d.ts.map +1 -1
- package/dist/src/trails/create.js +29 -29
- package/dist/src/trails/create.js.map +1 -1
- package/dist/src/trails/dev-clean.d.ts +9 -0
- package/dist/src/trails/dev-clean.d.ts.map +1 -0
- package/dist/src/trails/dev-clean.js +65 -0
- package/dist/src/trails/dev-clean.js.map +1 -0
- package/dist/src/trails/dev-reset.d.ts +6 -0
- package/dist/src/trails/dev-reset.d.ts.map +1 -0
- package/dist/src/trails/dev-reset.js +38 -0
- package/dist/src/trails/dev-reset.js.map +1 -0
- package/dist/src/trails/dev-stats.d.ts +7 -0
- package/dist/src/trails/dev-stats.d.ts.map +1 -0
- package/dist/src/trails/dev-stats.js +61 -0
- package/dist/src/trails/dev-stats.js.map +1 -0
- package/dist/src/trails/dev-support.d.ts +64 -0
- package/dist/src/trails/dev-support.d.ts.map +1 -0
- package/dist/src/trails/dev-support.js +178 -0
- package/dist/src/trails/dev-support.js.map +1 -0
- package/dist/src/trails/draft-promote.d.ts +18 -0
- package/dist/src/trails/draft-promote.d.ts.map +1 -0
- package/dist/src/trails/draft-promote.js +386 -0
- package/dist/src/trails/draft-promote.js.map +1 -0
- package/dist/src/trails/guide.d.ts +13 -3
- package/dist/src/trails/guide.d.ts.map +1 -1
- package/dist/src/trails/guide.js +21 -37
- package/dist/src/trails/guide.js.map +1 -1
- package/dist/src/trails/load-app.d.ts +3 -1
- package/dist/src/trails/load-app.d.ts.map +1 -1
- package/dist/src/trails/load-app.js +53 -10
- package/dist/src/trails/load-app.js.map +1 -1
- package/dist/src/trails/project.d.ts.map +1 -1
- package/dist/src/trails/project.js +14 -3
- package/dist/src/trails/project.js.map +1 -1
- package/dist/src/trails/survey.d.ts +4 -58
- package/dist/src/trails/survey.d.ts.map +1 -1
- package/dist/src/trails/survey.js +52 -173
- package/dist/src/trails/survey.js.map +1 -1
- package/dist/src/trails/topo-constants.d.ts +3 -0
- package/dist/src/trails/topo-constants.d.ts.map +1 -0
- package/dist/src/trails/topo-constants.js +3 -0
- package/dist/src/trails/topo-constants.js.map +1 -0
- package/dist/src/trails/topo-export.d.ts +18 -0
- package/dist/src/trails/topo-export.d.ts.map +1 -0
- package/dist/src/trails/topo-export.js +34 -0
- package/dist/src/trails/topo-export.js.map +1 -0
- package/dist/src/trails/topo-history.d.ts +24 -0
- package/dist/src/trails/topo-history.d.ts.map +1 -0
- package/dist/src/trails/topo-history.js +33 -0
- package/dist/src/trails/topo-history.js.map +1 -0
- package/dist/src/trails/topo-pin.d.ts +21 -0
- package/dist/src/trails/topo-pin.d.ts.map +1 -0
- package/dist/src/trails/topo-pin.js +35 -0
- package/dist/src/trails/topo-pin.js.map +1 -0
- package/dist/src/trails/topo-read-support.d.ts +54 -0
- package/dist/src/trails/topo-read-support.d.ts.map +1 -0
- package/dist/src/trails/topo-read-support.js +178 -0
- package/dist/src/trails/topo-read-support.js.map +1 -0
- package/dist/src/trails/topo-reports.d.ts +50 -0
- package/dist/src/trails/topo-reports.d.ts.map +1 -0
- package/dist/src/trails/topo-reports.js +122 -0
- package/dist/src/trails/topo-reports.js.map +1 -0
- package/dist/src/trails/topo-show.d.ts +23 -0
- package/dist/src/trails/topo-show.d.ts.map +1 -0
- package/dist/src/trails/topo-show.js +53 -0
- package/dist/src/trails/topo-show.js.map +1 -0
- package/dist/src/trails/topo-store-support.d.ts +13 -0
- package/dist/src/trails/topo-store-support.d.ts.map +1 -0
- package/dist/src/trails/topo-store-support.js +55 -0
- package/dist/src/trails/topo-store-support.js.map +1 -0
- package/dist/src/trails/topo-support.d.ts +87 -0
- package/dist/src/trails/topo-support.d.ts.map +1 -0
- package/dist/src/trails/topo-support.js +165 -0
- package/dist/src/trails/topo-support.js.map +1 -0
- package/dist/src/trails/topo-unpin.d.ts +15 -0
- package/dist/src/trails/topo-unpin.d.ts.map +1 -0
- package/dist/src/trails/topo-unpin.js +39 -0
- package/dist/src/trails/topo-unpin.js.map +1 -0
- package/dist/src/trails/topo-verify.d.ts +5 -0
- package/dist/src/trails/topo-verify.d.ts.map +1 -0
- package/dist/src/trails/topo-verify.js +28 -0
- package/dist/src/trails/topo-verify.js.map +1 -0
- package/dist/src/trails/topo.d.ts +5 -0
- package/dist/src/trails/topo.d.ts.map +1 -0
- package/dist/src/trails/topo.js +67 -0
- package/dist/src/trails/topo.js.map +1 -0
- package/dist/src/trails/warden.d.ts +1 -1
- package/dist/src/trails/warden.d.ts.map +1 -1
- package/dist/src/trails/warden.js +28 -27
- package/dist/src/trails/warden.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/__tests__/draft-promote.test.ts +144 -0
- package/src/__tests__/load-app.test.ts +43 -0
- package/src/__tests__/survey.test.ts +85 -0
- package/src/__tests__/topo-dev.test.ts +424 -0
- package/src/app.ts +22 -0
- package/src/trails/dev-clean.ts +73 -0
- package/src/trails/dev-reset.ts +44 -0
- package/src/trails/dev-stats.ts +64 -0
- package/src/trails/dev-support.ts +326 -0
- package/src/trails/draft-promote.ts +704 -0
- package/src/trails/guide.ts +22 -37
- package/src/trails/load-app.ts +76 -13
- package/src/trails/project.ts +17 -3
- package/src/trails/survey.ts +56 -256
- package/src/trails/topo-constants.ts +2 -0
- package/src/trails/topo-export.ts +39 -0
- package/src/trails/topo-history.ts +40 -0
- package/src/trails/topo-pin.ts +42 -0
- package/src/trails/topo-read-support.ts +332 -0
- package/src/trails/topo-reports.ts +221 -0
- package/src/trails/topo-show.ts +58 -0
- package/src/trails/topo-store-support.ts +96 -0
- package/src/trails/topo-support.ts +274 -0
- package/src/trails/topo-unpin.ts +51 -0
- package/src/trails/topo-verify.ts +29 -0
- package/src/trails/topo.ts +73 -0
- package/src/trails/warden.ts +1 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/* oxlint-disable max-statements */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import type { Result } from '@ontrails/core';
|
|
14
|
+
import { openReadTrailsDb } from '@ontrails/core/internal/trails-db';
|
|
15
|
+
import { createDevStore } from '@ontrails/tracker';
|
|
16
|
+
|
|
17
|
+
import { devCleanTrail } from '../trails/dev-clean.js';
|
|
18
|
+
import { devResetTrail } from '../trails/dev-reset.js';
|
|
19
|
+
import { devStatsTrail } from '../trails/dev-stats.js';
|
|
20
|
+
import { guideTrail } from '../trails/guide.js';
|
|
21
|
+
import { surveyTrail } from '../trails/survey.js';
|
|
22
|
+
import { topoExportTrail } from '../trails/topo-export.js';
|
|
23
|
+
import { topoHistoryTrail } from '../trails/topo-history.js';
|
|
24
|
+
import { topoPinTrail } from '../trails/topo-pin.js';
|
|
25
|
+
import { topoShowTrail } from '../trails/topo-show.js';
|
|
26
|
+
import { topoTrail } from '../trails/topo.js';
|
|
27
|
+
import { topoUnpinTrail } from '../trails/topo-unpin.js';
|
|
28
|
+
import { topoVerifyTrail } from '../trails/topo-verify.js';
|
|
29
|
+
|
|
30
|
+
const repoTempDir = (): string =>
|
|
31
|
+
join(
|
|
32
|
+
resolve(import.meta.dir, '../..'),
|
|
33
|
+
'.tmp-tests',
|
|
34
|
+
`topo-dev-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const expectOk = <T>(result: Result<T, Error>): T => {
|
|
38
|
+
if (result.isErr()) {
|
|
39
|
+
throw result.error;
|
|
40
|
+
}
|
|
41
|
+
return result.value;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const expectErr = <E extends Error>(result: Result<unknown, E>): E => {
|
|
45
|
+
if (result.isOk()) {
|
|
46
|
+
throw new Error('expected result to be an error');
|
|
47
|
+
}
|
|
48
|
+
return result.error;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const moduleInput = { module: './src/app.ts' } as const;
|
|
52
|
+
|
|
53
|
+
const writeAppFixture = (dir: string): void => {
|
|
54
|
+
mkdirSync(join(dir, 'src'), { recursive: true });
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(dir, 'src', 'app.ts'),
|
|
57
|
+
`import { Result, provision, topo, trail } from '@ontrails/core';
|
|
58
|
+
import { z } from 'zod';
|
|
59
|
+
|
|
60
|
+
const hello = trail('hello', {
|
|
61
|
+
blaze: async (input) => Result.ok({ message: \`Hello, \${input.name ?? 'world'}!\` }),
|
|
62
|
+
crosses: ['goodbye'],
|
|
63
|
+
examples: [{ input: {}, name: 'Default greeting' }],
|
|
64
|
+
input: z.object({ name: z.string().optional() }),
|
|
65
|
+
intent: 'read',
|
|
66
|
+
output: z.object({ message: z.string() }),
|
|
67
|
+
provisions: [
|
|
68
|
+
provision('db.main', {
|
|
69
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const goodbye = trail('goodbye', {
|
|
75
|
+
blaze: async () => Result.ok({ ok: true }),
|
|
76
|
+
input: z.object({}),
|
|
77
|
+
intent: 'write',
|
|
78
|
+
output: z.object({ ok: z.boolean() }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const [dbMain] = hello.provisions;
|
|
82
|
+
if (!dbMain) {
|
|
83
|
+
throw new Error('expected hello to declare db.main');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const app = topo('fixture-app', { dbMain, goodbye, hello });
|
|
87
|
+
`
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe('topo and dev trails', () => {
|
|
92
|
+
test('topo surfaces current summary, detail, and export/verify flow', async () => {
|
|
93
|
+
const dir = repoTempDir();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
writeAppFixture(dir);
|
|
97
|
+
|
|
98
|
+
const summary = expectOk(
|
|
99
|
+
await topoTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
100
|
+
);
|
|
101
|
+
expect(summary.app.name).toBe('fixture-app');
|
|
102
|
+
expect(summary.list.count).toBe(2);
|
|
103
|
+
expect(summary.list.provisionCount).toBe(1);
|
|
104
|
+
expect(summary.lockExists).toBe(false);
|
|
105
|
+
|
|
106
|
+
const detail = expectOk(
|
|
107
|
+
await topoShowTrail.blaze({ ...moduleInput, id: 'hello' }, {
|
|
108
|
+
cwd: dir,
|
|
109
|
+
} as never)
|
|
110
|
+
);
|
|
111
|
+
expect(detail.id).toBe('hello');
|
|
112
|
+
expect(detail.provisions).toEqual(['db.main']);
|
|
113
|
+
|
|
114
|
+
const exportResult = expectOk(
|
|
115
|
+
await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
116
|
+
);
|
|
117
|
+
expect(exportResult.hash).toHaveLength(64);
|
|
118
|
+
expect(existsSync(join(dir, '.trails', '_trailhead.json'))).toBe(true);
|
|
119
|
+
expect(existsSync(join(dir, '.trails', 'trails.lock'))).toBe(true);
|
|
120
|
+
expect(
|
|
121
|
+
JSON.parse(readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8'))
|
|
122
|
+
).toMatchObject({
|
|
123
|
+
hash: exportResult.hash,
|
|
124
|
+
version: 1,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(dir, '.trails', 'trailhead.lock'),
|
|
129
|
+
readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8')
|
|
130
|
+
);
|
|
131
|
+
rmSync(join(dir, '.trails', 'trails.lock'));
|
|
132
|
+
|
|
133
|
+
const legacySummary = expectOk(
|
|
134
|
+
await topoTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
135
|
+
);
|
|
136
|
+
expect(legacySummary.lockExists).toBe(true);
|
|
137
|
+
|
|
138
|
+
const verifyResult = expectOk(
|
|
139
|
+
await topoVerifyTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
140
|
+
);
|
|
141
|
+
expect(verifyResult.stale).toBe(false);
|
|
142
|
+
|
|
143
|
+
writeFileSync(join(dir, '.trails', 'trailhead.lock'), 'stale\n');
|
|
144
|
+
const verifyError = expectErr(
|
|
145
|
+
await topoVerifyTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
146
|
+
);
|
|
147
|
+
expect(verifyError.message).toContain('trails.lock is stale');
|
|
148
|
+
} finally {
|
|
149
|
+
rmSync(dir, { force: true, recursive: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('survey and guide read current topo state through the shared topo store', async () => {
|
|
154
|
+
const dir = repoTempDir();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
writeAppFixture(dir);
|
|
158
|
+
|
|
159
|
+
const surveyList = expectOk(
|
|
160
|
+
await surveyTrail.blaze({ module: './src/app.ts' }, {
|
|
161
|
+
cwd: dir,
|
|
162
|
+
} as never)
|
|
163
|
+
);
|
|
164
|
+
expect(surveyList).toMatchObject({
|
|
165
|
+
count: 2,
|
|
166
|
+
provisionCount: 1,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const surveyBrief = expectOk(
|
|
170
|
+
await surveyTrail.blaze({ brief: true, module: './src/app.ts' }, {
|
|
171
|
+
cwd: dir,
|
|
172
|
+
} as never)
|
|
173
|
+
);
|
|
174
|
+
expect(surveyBrief).toMatchObject({
|
|
175
|
+
features: {
|
|
176
|
+
examples: true,
|
|
177
|
+
outputSchemas: true,
|
|
178
|
+
provisions: true,
|
|
179
|
+
},
|
|
180
|
+
name: 'fixture-app',
|
|
181
|
+
trails: 2,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const surveyDetail = expectOk(
|
|
185
|
+
await surveyTrail.blaze({ module: './src/app.ts', trailId: 'hello' }, {
|
|
186
|
+
cwd: dir,
|
|
187
|
+
} as never)
|
|
188
|
+
);
|
|
189
|
+
expect(surveyDetail).toMatchObject({
|
|
190
|
+
id: 'hello',
|
|
191
|
+
provisions: ['db.main'],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const guideList = expectOk(
|
|
195
|
+
await guideTrail.blaze({ module: './src/app.ts' }, {
|
|
196
|
+
cwd: dir,
|
|
197
|
+
} as never)
|
|
198
|
+
);
|
|
199
|
+
expect(guideList).toEqual([
|
|
200
|
+
{
|
|
201
|
+
description: '(no description)',
|
|
202
|
+
exampleCount: 0,
|
|
203
|
+
id: 'goodbye',
|
|
204
|
+
kind: 'trail',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
description: '(no description)',
|
|
208
|
+
exampleCount: 1,
|
|
209
|
+
id: 'hello',
|
|
210
|
+
kind: 'trail',
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const guideDetail = expectOk(
|
|
215
|
+
await guideTrail.blaze({ module: './src/app.ts', trailId: 'hello' }, {
|
|
216
|
+
cwd: dir,
|
|
217
|
+
} as never)
|
|
218
|
+
);
|
|
219
|
+
expect(guideDetail).toMatchObject({
|
|
220
|
+
description: null,
|
|
221
|
+
examples: [
|
|
222
|
+
{
|
|
223
|
+
input: {},
|
|
224
|
+
name: 'Default greeting',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
id: 'hello',
|
|
228
|
+
kind: 'trail',
|
|
229
|
+
});
|
|
230
|
+
} finally {
|
|
231
|
+
rmSync(dir, { force: true, recursive: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('pinning, history, unpinning, and dev maintenance work against shared trails.db', async () => {
|
|
236
|
+
const dir = repoTempDir();
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
writeAppFixture(dir);
|
|
240
|
+
|
|
241
|
+
const firstPin = expectOk(
|
|
242
|
+
await topoPinTrail.blaze({ ...moduleInput, name: 'before-auth' }, {
|
|
243
|
+
cwd: dir,
|
|
244
|
+
} as never)
|
|
245
|
+
);
|
|
246
|
+
expect(firstPin.pin.name).toBe('before-auth');
|
|
247
|
+
expect(firstPin.pin.saveId).toBe(firstPin.save.id);
|
|
248
|
+
|
|
249
|
+
const firstExport = expectOk(
|
|
250
|
+
await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
251
|
+
);
|
|
252
|
+
const secondExport = expectOk(
|
|
253
|
+
await topoExportTrail.blaze(moduleInput, { cwd: dir } as never)
|
|
254
|
+
);
|
|
255
|
+
expect(firstExport.hash).toBe(secondExport.hash);
|
|
256
|
+
expect(
|
|
257
|
+
JSON.parse(readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8'))
|
|
258
|
+
).toMatchObject({
|
|
259
|
+
hash: secondExport.hash,
|
|
260
|
+
version: 1,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const projectionDb = openReadTrailsDb({ rootDir: dir });
|
|
264
|
+
try {
|
|
265
|
+
const pinnedRows = projectionDb
|
|
266
|
+
.query<{ count: number }, [string]>(
|
|
267
|
+
'SELECT COUNT(*) as count FROM topo_trails WHERE save_id = ?'
|
|
268
|
+
)
|
|
269
|
+
.get(firstPin.save.id);
|
|
270
|
+
const exportedRows = projectionDb
|
|
271
|
+
.query<{ count: number }, [string]>(
|
|
272
|
+
'SELECT COUNT(*) as count FROM topo_trails WHERE save_id = ?'
|
|
273
|
+
)
|
|
274
|
+
.get(firstExport.save.id);
|
|
275
|
+
const projectedSaves = projectionDb
|
|
276
|
+
.query<{ count: number }, []>(
|
|
277
|
+
'SELECT COUNT(DISTINCT save_id) as count FROM topo_trails'
|
|
278
|
+
)
|
|
279
|
+
.get();
|
|
280
|
+
const cachedSchemas = projectionDb
|
|
281
|
+
.query<{ count: number }, []>(
|
|
282
|
+
'SELECT COUNT(*) as count FROM topo_schemas'
|
|
283
|
+
)
|
|
284
|
+
.get();
|
|
285
|
+
|
|
286
|
+
expect(pinnedRows?.count).toBe(2);
|
|
287
|
+
expect(exportedRows?.count).toBe(2);
|
|
288
|
+
expect(projectedSaves?.count).toBe(3);
|
|
289
|
+
expect(cachedSchemas?.count).toBeGreaterThanOrEqual(9);
|
|
290
|
+
} finally {
|
|
291
|
+
projectionDb.close();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const store = createDevStore({ rootDir: dir });
|
|
295
|
+
try {
|
|
296
|
+
store.write({
|
|
297
|
+
attrs: {},
|
|
298
|
+
endedAt: Date.now() - 1000,
|
|
299
|
+
id: 'track-1',
|
|
300
|
+
kind: 'trail',
|
|
301
|
+
name: 'hello',
|
|
302
|
+
rootId: 'track-1',
|
|
303
|
+
startedAt: Date.now() - 10_000,
|
|
304
|
+
status: 'ok',
|
|
305
|
+
traceId: 'trace-1',
|
|
306
|
+
trailId: 'hello',
|
|
307
|
+
trailhead: 'cli',
|
|
308
|
+
});
|
|
309
|
+
store.write({
|
|
310
|
+
attrs: {},
|
|
311
|
+
endedAt: Date.now() - 500,
|
|
312
|
+
id: 'track-2',
|
|
313
|
+
kind: 'trail',
|
|
314
|
+
name: 'goodbye',
|
|
315
|
+
rootId: 'track-2',
|
|
316
|
+
startedAt: Date.now() - 20_000,
|
|
317
|
+
status: 'err',
|
|
318
|
+
traceId: 'trace-2',
|
|
319
|
+
trailId: 'goodbye',
|
|
320
|
+
trailhead: 'cli',
|
|
321
|
+
});
|
|
322
|
+
} finally {
|
|
323
|
+
store.close();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const history = expectOk(
|
|
327
|
+
await topoHistoryTrail.blaze({}, { cwd: dir } as never)
|
|
328
|
+
);
|
|
329
|
+
expect(history.pinCount).toBe(1);
|
|
330
|
+
expect(history.saveCount).toBeGreaterThanOrEqual(3);
|
|
331
|
+
expect(history.pins[0]?.saveId).toBe(firstPin.save.id);
|
|
332
|
+
expect(
|
|
333
|
+
history.saves.some((save) => save.id === secondExport.save.id)
|
|
334
|
+
).toBe(true);
|
|
335
|
+
|
|
336
|
+
const stats = expectOk(
|
|
337
|
+
await devStatsTrail.blaze({}, { cwd: dir } as never)
|
|
338
|
+
);
|
|
339
|
+
expect(stats.topo.pinCount).toBe(1);
|
|
340
|
+
expect(stats.tracker.recordCount).toBe(2);
|
|
341
|
+
|
|
342
|
+
const cleanPreview = expectOk(
|
|
343
|
+
await devCleanTrail.blaze({ dryRun: true, saves: 0, trackAgeMs: 0 }, {
|
|
344
|
+
cwd: dir,
|
|
345
|
+
} as never)
|
|
346
|
+
);
|
|
347
|
+
expect(cleanPreview.dryRun).toBe(true);
|
|
348
|
+
expect(cleanPreview.removed.topoSaves).toBeGreaterThanOrEqual(2);
|
|
349
|
+
expect(cleanPreview.removed.trackRecords).toBe(2);
|
|
350
|
+
|
|
351
|
+
const cleanResult = expectOk(
|
|
352
|
+
await devCleanTrail.blaze(
|
|
353
|
+
{ dryRun: false, saves: 0, trackAgeMs: 0, yes: true },
|
|
354
|
+
{ cwd: dir } as never
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
expect(cleanResult.removed.trackRecords).toBe(2);
|
|
358
|
+
expect(cleanResult.remaining.pinCount).toBe(1);
|
|
359
|
+
|
|
360
|
+
const unpinPreview = expectOk(
|
|
361
|
+
await topoUnpinTrail.blaze({ dryRun: true, name: 'before-auth' }, {
|
|
362
|
+
cwd: dir,
|
|
363
|
+
} as never)
|
|
364
|
+
);
|
|
365
|
+
expect(unpinPreview.dryRun).toBe(true);
|
|
366
|
+
expect(unpinPreview.pin?.name).toBe('before-auth');
|
|
367
|
+
|
|
368
|
+
const unpinResult = expectOk(
|
|
369
|
+
await topoUnpinTrail.blaze(
|
|
370
|
+
{ dryRun: false, name: 'before-auth', yes: true },
|
|
371
|
+
{ cwd: dir } as never
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
expect(unpinResult.removed).toBe(true);
|
|
375
|
+
|
|
376
|
+
const resetPreview = expectOk(
|
|
377
|
+
await devResetTrail.blaze({ dryRun: true }, { cwd: dir } as never)
|
|
378
|
+
);
|
|
379
|
+
expect(resetPreview.dryRun).toBe(true);
|
|
380
|
+
expect(resetPreview.removedFiles).toContain('.trails/trails.db');
|
|
381
|
+
|
|
382
|
+
const resetResult = expectOk(
|
|
383
|
+
await devResetTrail.blaze({ dryRun: false, yes: true }, {
|
|
384
|
+
cwd: dir,
|
|
385
|
+
} as never)
|
|
386
|
+
);
|
|
387
|
+
expect(resetResult.removedFiles).toContain('.trails/trails.db');
|
|
388
|
+
expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
|
|
389
|
+
expect(
|
|
390
|
+
readFileSync(join(dir, '.trails', 'trails.lock'), 'utf8').length
|
|
391
|
+
).toBeGreaterThan(0);
|
|
392
|
+
} finally {
|
|
393
|
+
rmSync(dir, { force: true, recursive: true });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('dev clean stays side-effect free when no local state exists', async () => {
|
|
398
|
+
const dir = repoTempDir();
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
mkdirSync(dir, { recursive: true });
|
|
402
|
+
|
|
403
|
+
const preview = expectOk(
|
|
404
|
+
await devCleanTrail.blaze({ dryRun: true }, { cwd: dir } as never)
|
|
405
|
+
);
|
|
406
|
+
expect(preview.dryRun).toBe(true);
|
|
407
|
+
expect(preview.removed.topoSaves).toBe(0);
|
|
408
|
+
expect(preview.removed.trackRecords).toBe(0);
|
|
409
|
+
expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
|
|
410
|
+
|
|
411
|
+
const applied = expectOk(
|
|
412
|
+
await devCleanTrail.blaze({ dryRun: false, yes: true }, {
|
|
413
|
+
cwd: dir,
|
|
414
|
+
} as never)
|
|
415
|
+
);
|
|
416
|
+
expect(applied.dryRun).toBe(false);
|
|
417
|
+
expect(applied.removed.topoSaves).toBe(0);
|
|
418
|
+
expect(applied.removed.trackRecords).toBe(0);
|
|
419
|
+
expect(existsSync(join(dir, '.trails', 'trails.db'))).toBe(false);
|
|
420
|
+
} finally {
|
|
421
|
+
rmSync(dir, { force: true, recursive: true });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
});
|
package/src/app.ts
CHANGED
|
@@ -5,14 +5,36 @@ import * as addTrail from './trails/add-trail.js';
|
|
|
5
5
|
import * as addVerify from './trails/add-verify.js';
|
|
6
6
|
import * as create from './trails/create.js';
|
|
7
7
|
import * as createScaffold from './trails/create-scaffold.js';
|
|
8
|
+
import * as devClean from './trails/dev-clean.js';
|
|
9
|
+
import * as devReset from './trails/dev-reset.js';
|
|
10
|
+
import * as devStats from './trails/dev-stats.js';
|
|
11
|
+
import * as draftPromote from './trails/draft-promote.js';
|
|
8
12
|
import * as guide from './trails/guide.js';
|
|
9
13
|
import * as survey from './trails/survey.js';
|
|
14
|
+
import * as topoExport from './trails/topo-export.js';
|
|
15
|
+
import * as topoHistory from './trails/topo-history.js';
|
|
16
|
+
import * as topoPin from './trails/topo-pin.js';
|
|
17
|
+
import * as topoShow from './trails/topo-show.js';
|
|
18
|
+
import * as topoCommand from './trails/topo.js';
|
|
19
|
+
import * as topoUnpin from './trails/topo-unpin.js';
|
|
20
|
+
import * as topoVerify from './trails/topo-verify.js';
|
|
10
21
|
import * as warden from './trails/warden.js';
|
|
11
22
|
|
|
12
23
|
export const app = topo(
|
|
13
24
|
'trails',
|
|
14
25
|
survey,
|
|
26
|
+
topoCommand,
|
|
27
|
+
topoShow,
|
|
28
|
+
topoHistory,
|
|
29
|
+
topoPin,
|
|
30
|
+
topoUnpin,
|
|
31
|
+
topoExport,
|
|
32
|
+
topoVerify,
|
|
33
|
+
devStats,
|
|
34
|
+
devClean,
|
|
35
|
+
devReset,
|
|
15
36
|
guide,
|
|
37
|
+
draftPromote,
|
|
16
38
|
warden,
|
|
17
39
|
create,
|
|
18
40
|
createScaffold,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Result, ValidationError, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { cleanDevState, DEFAULT_TOPO_SAVE_RETENTION } from './dev-support.js';
|
|
5
|
+
import { isolatedExampleInput } from './topo-support.js';
|
|
6
|
+
|
|
7
|
+
export const devCleanTrail = trail('dev.clean', {
|
|
8
|
+
blaze: (input, ctx) => {
|
|
9
|
+
if (input.dryRun !== true && input.yes !== true) {
|
|
10
|
+
return Result.err(
|
|
11
|
+
new ValidationError(
|
|
12
|
+
'Refusing to clean local state without `--yes` or `--dry-run`.'
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
18
|
+
return Result.ok(
|
|
19
|
+
cleanDevState({
|
|
20
|
+
dryRun: input.dryRun,
|
|
21
|
+
maxAge: input.trackAgeMs,
|
|
22
|
+
maxRecords: input.tracks,
|
|
23
|
+
rootDir,
|
|
24
|
+
saveRetention: input.saves,
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
description: 'Prune unpinned topo saves and old track records',
|
|
29
|
+
examples: [
|
|
30
|
+
{
|
|
31
|
+
input: {
|
|
32
|
+
dryRun: true,
|
|
33
|
+
rootDir: isolatedExampleInput('dev-clean').rootDir,
|
|
34
|
+
},
|
|
35
|
+
name: 'Preview local cleanup',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
input: z.object({
|
|
39
|
+
dryRun: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.default(true)
|
|
42
|
+
.describe('Preview cleanup without changing state'),
|
|
43
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
44
|
+
saves: z
|
|
45
|
+
.number()
|
|
46
|
+
.default(DEFAULT_TOPO_SAVE_RETENTION)
|
|
47
|
+
.describe('Unpinned topo saves to retain'),
|
|
48
|
+
trackAgeMs: z
|
|
49
|
+
.number()
|
|
50
|
+
.default(7 * 24 * 60 * 60 * 1000)
|
|
51
|
+
.describe('Maximum retained track age in milliseconds'),
|
|
52
|
+
tracks: z.number().default(10_000).describe('Maximum retained track count'),
|
|
53
|
+
yes: z.boolean().default(false).describe('Confirm destructive changes'),
|
|
54
|
+
}),
|
|
55
|
+
intent: 'destroy',
|
|
56
|
+
output: z.object({
|
|
57
|
+
dryRun: z.boolean(),
|
|
58
|
+
remaining: z.object({
|
|
59
|
+
pinCount: z.number(),
|
|
60
|
+
saveCount: z.number(),
|
|
61
|
+
trackCount: z.number(),
|
|
62
|
+
}),
|
|
63
|
+
removed: z.object({
|
|
64
|
+
topoSaves: z.number(),
|
|
65
|
+
trackRecords: z.number(),
|
|
66
|
+
}),
|
|
67
|
+
retention: z.object({
|
|
68
|
+
saves: z.number(),
|
|
69
|
+
trackAgeMs: z.number(),
|
|
70
|
+
tracks: z.number(),
|
|
71
|
+
}),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Result, ValidationError, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { resetDevState } from './dev-support.js';
|
|
5
|
+
import { isolatedExampleInput } from './topo-support.js';
|
|
6
|
+
|
|
7
|
+
export const devResetTrail = trail('dev.reset', {
|
|
8
|
+
blaze: (input, ctx) => {
|
|
9
|
+
if (input.dryRun !== true && input.yes !== true) {
|
|
10
|
+
return Result.err(
|
|
11
|
+
new ValidationError(
|
|
12
|
+
'Refusing to reset local state without `--yes` or `--dry-run`.'
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
18
|
+
return Result.ok(resetDevState({ dryRun: input.dryRun, rootDir }));
|
|
19
|
+
},
|
|
20
|
+
description: 'Remove local Trails database artifacts',
|
|
21
|
+
examples: [
|
|
22
|
+
{
|
|
23
|
+
input: {
|
|
24
|
+
dryRun: true,
|
|
25
|
+
rootDir: isolatedExampleInput('dev-reset').rootDir,
|
|
26
|
+
},
|
|
27
|
+
name: 'Preview local reset',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
input: z.object({
|
|
31
|
+
dryRun: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.default(true)
|
|
34
|
+
.describe('Preview reset without changing state'),
|
|
35
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
36
|
+
yes: z.boolean().default(false).describe('Confirm destructive changes'),
|
|
37
|
+
}),
|
|
38
|
+
intent: 'destroy',
|
|
39
|
+
output: z.object({
|
|
40
|
+
dryRun: z.boolean(),
|
|
41
|
+
removedCount: z.number(),
|
|
42
|
+
removedFiles: z.array(z.string()),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Result, trail } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { buildDevStats, DEFAULT_TOPO_SAVE_RETENTION } from './dev-support.js';
|
|
5
|
+
import { isolatedExampleInput } from './topo-support.js';
|
|
6
|
+
|
|
7
|
+
export const devStatsTrail = trail('dev.stats', {
|
|
8
|
+
blaze: (input, ctx) => {
|
|
9
|
+
const rootDir = input.rootDir ?? ctx.cwd ?? process.cwd();
|
|
10
|
+
return Result.ok(
|
|
11
|
+
buildDevStats({
|
|
12
|
+
maxAge: input.trackAgeMs,
|
|
13
|
+
maxRecords: input.tracks,
|
|
14
|
+
rootDir,
|
|
15
|
+
saveRetention: input.saves,
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
description: 'Show local Trails workspace state and retention',
|
|
20
|
+
examples: [
|
|
21
|
+
{
|
|
22
|
+
input: { rootDir: isolatedExampleInput('dev-stats').rootDir },
|
|
23
|
+
name: 'Show local dev state',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
input: z.object({
|
|
27
|
+
rootDir: z.string().optional().describe('Workspace root directory'),
|
|
28
|
+
saves: z
|
|
29
|
+
.number()
|
|
30
|
+
.default(DEFAULT_TOPO_SAVE_RETENTION)
|
|
31
|
+
.describe('Unpinned topo saves to retain'),
|
|
32
|
+
trackAgeMs: z
|
|
33
|
+
.number()
|
|
34
|
+
.default(7 * 24 * 60 * 60 * 1000)
|
|
35
|
+
.describe('Maximum retained track age in milliseconds'),
|
|
36
|
+
tracks: z.number().default(10_000).describe('Maximum retained track count'),
|
|
37
|
+
}),
|
|
38
|
+
intent: 'read',
|
|
39
|
+
output: z.object({
|
|
40
|
+
db: z.object({
|
|
41
|
+
exists: z.boolean(),
|
|
42
|
+
fileSizeBytes: z.number(),
|
|
43
|
+
path: z.string(),
|
|
44
|
+
}),
|
|
45
|
+
lock: z.object({
|
|
46
|
+
exists: z.boolean(),
|
|
47
|
+
fileSizeBytes: z.number(),
|
|
48
|
+
path: z.string(),
|
|
49
|
+
}),
|
|
50
|
+
retention: z.object({
|
|
51
|
+
saves: z.number(),
|
|
52
|
+
trackAgeMs: z.number(),
|
|
53
|
+
tracks: z.number(),
|
|
54
|
+
}),
|
|
55
|
+
topo: z.object({
|
|
56
|
+
pinCount: z.number(),
|
|
57
|
+
prunableSaveCount: z.number(),
|
|
58
|
+
saveCount: z.number(),
|
|
59
|
+
}),
|
|
60
|
+
tracker: z.object({
|
|
61
|
+
recordCount: z.number(),
|
|
62
|
+
}),
|
|
63
|
+
}),
|
|
64
|
+
});
|