lynkeus 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__tests__/args.test.d.ts +1 -0
- package/lib/__tests__/args.test.js +69 -0
- package/lib/__tests__/check.test.d.ts +1 -0
- package/lib/__tests__/check.test.js +60 -0
- package/lib/__tests__/init.test.d.ts +1 -0
- package/lib/__tests__/init.test.js +42 -0
- package/lib/__tests__/record.test.d.ts +1 -0
- package/lib/__tests__/record.test.js +44 -0
- package/lib/__tests__/snapshot.test.d.ts +1 -0
- package/lib/__tests__/snapshot.test.js +47 -0
- package/lib/__tests__/targets.test.d.ts +1 -0
- package/lib/__tests__/targets.test.js +127 -0
- package/lib/args.d.ts +30 -0
- package/lib/args.js +86 -0
- package/lib/cases/__tests__/format.test.d.ts +1 -0
- package/lib/cases/__tests__/format.test.js +61 -0
- package/lib/cases/__tests__/runner.test.d.ts +1 -0
- package/lib/cases/__tests__/runner.test.js +188 -0
- package/lib/cases/fixtures.d.ts +13 -0
- package/lib/cases/fixtures.js +100 -0
- package/lib/cases/format.d.ts +37 -0
- package/lib/cases/format.js +169 -0
- package/lib/cases/runner.d.ts +61 -0
- package/lib/cases/runner.js +590 -0
- package/lib/cases/templates.d.ts +20 -0
- package/lib/cases/templates.js +156 -0
- package/lib/cli.d.ts +6 -0
- package/lib/cli.js +173 -0
- package/lib/commands/cases.d.ts +1 -0
- package/lib/commands/cases.js +179 -0
- package/lib/commands/check.d.ts +36 -0
- package/lib/commands/check.js +67 -0
- package/lib/commands/crawl/return.d.ts +20 -0
- package/lib/commands/crawl/return.js +125 -0
- package/lib/commands/crawl/state.d.ts +30 -0
- package/lib/commands/crawl/state.js +56 -0
- package/lib/commands/crawl.d.ts +67 -0
- package/lib/commands/crawl.js +213 -0
- package/lib/commands/device.d.ts +10 -0
- package/lib/commands/device.js +122 -0
- package/lib/commands/doctor.d.ts +3 -0
- package/lib/commands/doctor.js +240 -0
- package/lib/commands/drive.d.ts +1 -0
- package/lib/commands/drive.js +410 -0
- package/lib/commands/flake.d.ts +14 -0
- package/lib/commands/flake.js +42 -0
- package/lib/commands/headless.d.ts +1 -0
- package/lib/commands/headless.js +127 -0
- package/lib/commands/init.d.ts +13 -0
- package/lib/commands/init.js +85 -0
- package/lib/commands/map.d.ts +2 -0
- package/lib/commands/map.js +297 -0
- package/lib/commands/os.d.ts +1 -0
- package/lib/commands/os.js +193 -0
- package/lib/commands/overlay.d.ts +9 -0
- package/lib/commands/overlay.js +12 -0
- package/lib/commands/record.d.ts +5 -0
- package/lib/commands/record.js +55 -0
- package/lib/commands/runs.d.ts +1 -0
- package/lib/commands/runs.js +452 -0
- package/lib/commands/smoke.d.ts +28 -0
- package/lib/commands/smoke.js +59 -0
- package/lib/commands/snapshot.d.ts +27 -0
- package/lib/commands/snapshot.js +69 -0
- package/lib/config.d.ts +72 -0
- package/lib/config.js +44 -0
- package/lib/device/devices.d.ts +11 -0
- package/lib/device/devices.js +33 -0
- package/lib/device/driver.d.ts +53 -0
- package/lib/device/driver.js +169 -0
- package/lib/device/platform.d.ts +7 -0
- package/lib/device/platform.js +59 -0
- package/lib/files.d.ts +7 -0
- package/lib/files.js +29 -0
- package/lib/flow/artifacts.d.ts +19 -0
- package/lib/flow/artifacts.js +49 -0
- package/lib/flow/runner.d.ts +13 -0
- package/lib/flow/runner.js +169 -0
- package/lib/flow/types.d.ts +111 -0
- package/lib/flow/types.js +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +11 -0
- package/lib/knowledge/__tests__/edges.test.d.ts +1 -0
- package/lib/knowledge/__tests__/edges.test.js +25 -0
- package/lib/knowledge/__tests__/events.test.d.ts +1 -0
- package/lib/knowledge/__tests__/events.test.js +39 -0
- package/lib/knowledge/__tests__/extract.test.d.ts +1 -0
- package/lib/knowledge/__tests__/extract.test.js +69 -0
- package/lib/knowledge/__tests__/frontier.test.d.ts +1 -0
- package/lib/knowledge/__tests__/frontier.test.js +57 -0
- package/lib/knowledge/__tests__/funnels.test.d.ts +1 -0
- package/lib/knowledge/__tests__/funnels.test.js +40 -0
- package/lib/knowledge/__tests__/lint.test.d.ts +1 -0
- package/lib/knowledge/__tests__/lint.test.js +53 -0
- package/lib/knowledge/__tests__/perf.test.d.ts +1 -0
- package/lib/knowledge/__tests__/perf.test.js +34 -0
- package/lib/knowledge/__tests__/plan.test.d.ts +1 -0
- package/lib/knowledge/__tests__/plan.test.js +35 -0
- package/lib/knowledge/__tests__/query.test.d.ts +1 -0
- package/lib/knowledge/__tests__/query.test.js +55 -0
- package/lib/knowledge/__tests__/screens.test.d.ts +1 -0
- package/lib/knowledge/__tests__/screens.test.js +30 -0
- package/lib/knowledge/__tests__/why.test.d.ts +1 -0
- package/lib/knowledge/__tests__/why.test.js +28 -0
- package/lib/knowledge/edges.d.ts +30 -0
- package/lib/knowledge/edges.js +68 -0
- package/lib/knowledge/errors.d.ts +8 -0
- package/lib/knowledge/errors.js +62 -0
- package/lib/knowledge/events.d.ts +23 -0
- package/lib/knowledge/events.js +51 -0
- package/lib/knowledge/extract.d.ts +26 -0
- package/lib/knowledge/extract.js +365 -0
- package/lib/knowledge/frontier.d.ts +26 -0
- package/lib/knowledge/frontier.js +60 -0
- package/lib/knowledge/funnels.d.ts +46 -0
- package/lib/knowledge/funnels.js +103 -0
- package/lib/knowledge/lint.d.ts +16 -0
- package/lib/knowledge/lint.js +89 -0
- package/lib/knowledge/perf.d.ts +32 -0
- package/lib/knowledge/perf.js +91 -0
- package/lib/knowledge/plan.d.ts +22 -0
- package/lib/knowledge/plan.js +81 -0
- package/lib/knowledge/problems.d.ts +5 -0
- package/lib/knowledge/problems.js +8 -0
- package/lib/knowledge/query.d.ts +36 -0
- package/lib/knowledge/query.js +127 -0
- package/lib/knowledge/route-params.d.ts +6 -0
- package/lib/knowledge/route-params.js +42 -0
- package/lib/knowledge/screens.d.ts +30 -0
- package/lib/knowledge/screens.js +125 -0
- package/lib/knowledge/tree.d.ts +15 -0
- package/lib/knowledge/tree.js +63 -0
- package/lib/knowledge/types.d.ts +62 -0
- package/lib/knowledge/types.js +6 -0
- package/lib/knowledge/why.d.ts +24 -0
- package/lib/knowledge/why.js +153 -0
- package/lib/mcp/server.d.ts +4 -0
- package/lib/mcp/server.js +111 -0
- package/lib/registry.d.ts +78 -0
- package/lib/registry.js +113 -0
- package/lib/runtime.d.ts +12 -0
- package/lib/runtime.js +56 -0
- package/lib/targets.d.ts +17 -0
- package/lib/targets.js +43 -0
- package/package.json +47 -0
- package/src/__tests__/args.test.ts +82 -0
- package/src/__tests__/check.test.ts +71 -0
- package/src/__tests__/init.test.ts +52 -0
- package/src/__tests__/record.test.ts +49 -0
- package/src/__tests__/snapshot.test.ts +51 -0
- package/src/__tests__/targets.test.ts +132 -0
- package/src/args.ts +111 -0
- package/src/cases/__tests__/format.test.ts +76 -0
- package/src/cases/__tests__/runner.test.ts +230 -0
- package/src/cases/fixtures.ts +104 -0
- package/src/cases/format.ts +183 -0
- package/src/cases/runner.ts +575 -0
- package/src/cases/templates.ts +159 -0
- package/src/cli.ts +176 -0
- package/src/commands/cases.ts +170 -0
- package/src/commands/check.ts +83 -0
- package/src/commands/crawl/return.ts +130 -0
- package/src/commands/crawl/state.ts +70 -0
- package/src/commands/crawl.ts +262 -0
- package/src/commands/device.ts +119 -0
- package/src/commands/doctor.ts +237 -0
- package/src/commands/drive.ts +413 -0
- package/src/commands/flake.ts +48 -0
- package/src/commands/headless.ts +131 -0
- package/src/commands/init.ts +98 -0
- package/src/commands/map.ts +298 -0
- package/src/commands/os.ts +195 -0
- package/src/commands/overlay.ts +20 -0
- package/src/commands/record.ts +46 -0
- package/src/commands/runs.ts +475 -0
- package/src/commands/smoke.ts +86 -0
- package/src/commands/snapshot.ts +87 -0
- package/src/config.ts +103 -0
- package/src/device/devices.ts +44 -0
- package/src/device/driver.ts +197 -0
- package/src/device/platform.ts +74 -0
- package/src/files.ts +35 -0
- package/src/flow/artifacts.ts +61 -0
- package/src/flow/runner.ts +182 -0
- package/src/flow/types.ts +55 -0
- package/src/index.ts +18 -0
- package/src/knowledge/__tests__/edges.test.ts +34 -0
- package/src/knowledge/__tests__/events.test.ts +45 -0
- package/src/knowledge/__tests__/extract.test.ts +88 -0
- package/src/knowledge/__tests__/frontier.test.ts +77 -0
- package/src/knowledge/__tests__/funnels.test.ts +58 -0
- package/src/knowledge/__tests__/lint.test.ts +66 -0
- package/src/knowledge/__tests__/perf.test.ts +41 -0
- package/src/knowledge/__tests__/plan.test.ts +52 -0
- package/src/knowledge/__tests__/query.test.ts +74 -0
- package/src/knowledge/__tests__/screens.test.ts +37 -0
- package/src/knowledge/__tests__/why.test.ts +40 -0
- package/src/knowledge/edges.ts +91 -0
- package/src/knowledge/errors.ts +73 -0
- package/src/knowledge/events.ts +60 -0
- package/src/knowledge/extract.ts +379 -0
- package/src/knowledge/frontier.ts +82 -0
- package/src/knowledge/funnels.ts +126 -0
- package/src/knowledge/lint.ts +99 -0
- package/src/knowledge/perf.ts +114 -0
- package/src/knowledge/plan.ts +95 -0
- package/src/knowledge/problems.ts +12 -0
- package/src/knowledge/query.ts +146 -0
- package/src/knowledge/route-params.ts +39 -0
- package/src/knowledge/screens.ts +139 -0
- package/src/knowledge/tree.ts +66 -0
- package/src/knowledge/types.ts +60 -0
- package/src/knowledge/why.ts +189 -0
- package/src/mcp/server.ts +109 -0
- package/src/registry.ts +191 -0
- package/src/runtime.ts +54 -0
- package/src/targets.ts +44 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A case is a markdown file a person can read: YAML frontmatter that says what
|
|
3
|
+
* it is, and a YAML body that says what to prepare (`setup`), what to do
|
|
4
|
+
* (`steps`) and what that proves (`expected`). Each step is one key: an
|
|
5
|
+
* `app.<verb>` drives the app, `assert` reads the backend, `exec` runs a shell
|
|
6
|
+
* command, and any other name is a fixture the project declared.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
|
|
10
|
+
import { parse } from 'yaml';
|
|
11
|
+
|
|
12
|
+
export type StepValue = string | number | boolean | null | Record<string, unknown> | unknown[];
|
|
13
|
+
export type Step = { verb: string; value: StepValue; on?: string };
|
|
14
|
+
|
|
15
|
+
export type Case = {
|
|
16
|
+
file: string;
|
|
17
|
+
id: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
meta: Record<string, unknown>;
|
|
20
|
+
setup: Step[];
|
|
21
|
+
steps: Step[];
|
|
22
|
+
expected?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A bare verb, or one `verb: value`. */
|
|
26
|
+
export const stepOf = (item: unknown, where = 'a step'): Step => {
|
|
27
|
+
if (typeof item === 'string') return { verb: item, value: null };
|
|
28
|
+
const keys = item && typeof item === 'object' ? Object.keys(item) : [];
|
|
29
|
+
if (keys.length !== 1) throw new Error(`${where} must be one step, as \`verb: value\`, not ${JSON.stringify(item)}`);
|
|
30
|
+
return { verb: keys[0]!, value: (item as Record<string, StepValue>)[keys[0]!]! };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const stepsOf = (list: unknown, file: string, section: string): Step[] => {
|
|
34
|
+
if (list === undefined || list === null) return [];
|
|
35
|
+
if (!Array.isArray(list)) throw new Error(`${file}: ${section} is not a list`);
|
|
36
|
+
return list.flatMap((item, i) => {
|
|
37
|
+
const entry = item as Record<string, unknown> | null;
|
|
38
|
+
if (entry && typeof entry === 'object' && 'do' in entry) {
|
|
39
|
+
if (typeof entry.on !== 'string') throw new Error(`${file}: ${section}[${i}] has \`do\` without \`on\``);
|
|
40
|
+
const inner = Array.isArray(entry.do) ? entry.do : [entry.do];
|
|
41
|
+
return inner.map((s, j) => ({ ...stepOf(s, `${file}: ${section}[${i}].do[${j}]`), on: entry.on as string }));
|
|
42
|
+
}
|
|
43
|
+
return [stepOf(item, `${file}: ${section}[${i}]`)];
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const parseCase = (text: string, file: string): Case => {
|
|
48
|
+
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
49
|
+
if (!match) throw new Error(`${file}: no frontmatter (a case starts with --- and its id)`);
|
|
50
|
+
const meta = (parse(match[1]!) ?? {}) as Record<string, unknown>;
|
|
51
|
+
const body = (parse(match[2]!) ?? {}) as Record<string, unknown>;
|
|
52
|
+
const id = typeof meta.id === 'string' ? meta.id : file.replace(/^.*\//, '').replace(/\.md$/, '');
|
|
53
|
+
return {
|
|
54
|
+
file,
|
|
55
|
+
id,
|
|
56
|
+
title: typeof meta.title === 'string' ? meta.title : undefined,
|
|
57
|
+
meta,
|
|
58
|
+
setup: stepsOf(body.setup, file, 'setup'),
|
|
59
|
+
steps: stepsOf(body.steps, file, 'steps'),
|
|
60
|
+
expected: typeof body.expected === 'string' ? body.expected.trim() : undefined,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const variantsOf = (text: string): Record<string, unknown>[] => {
|
|
65
|
+
const head = /^---\n([\s\S]*?)\n---\n/.exec(text);
|
|
66
|
+
const matrix = head ? ((parse(head[1]!) ?? {}) as Record<string, unknown>).matrix : undefined;
|
|
67
|
+
if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix) || Object.keys(matrix).length === 0) return [];
|
|
68
|
+
return Object.entries(matrix as Record<string, unknown>).reduce<Record<string, unknown>[]>(
|
|
69
|
+
(combos, [key, values]) => combos.flatMap((combo) => (Array.isArray(values) ? values : [values]).map((v) => ({ ...combo, [key]: v }))),
|
|
70
|
+
[{}],
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const readCases = (file: string): Case[] => {
|
|
75
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
76
|
+
const variants = variantsOf(text);
|
|
77
|
+
if (variants.length === 0) return [parseCase(text, file)];
|
|
78
|
+
return variants.map((variant) => {
|
|
79
|
+
const filled = text.replace(/\{\{\s*matrix\.(\w+)\s*\}\}/g, (ref, key: string) => {
|
|
80
|
+
if (!(key in variant)) throw new Error(`${file}: ${ref} is not in its matrix`);
|
|
81
|
+
return String(variant[key]);
|
|
82
|
+
});
|
|
83
|
+
const c = parseCase(filled, file);
|
|
84
|
+
return {
|
|
85
|
+
...c,
|
|
86
|
+
id: `${c.id}[${Object.entries(variant)
|
|
87
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
88
|
+
.join(',')}]`,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** `a.b.0.c` into a value; `a.*.b` collects b from every element of the list a. */
|
|
94
|
+
export const dig = (json: unknown, dotted: string): unknown => {
|
|
95
|
+
const star = dotted.indexOf('.*.');
|
|
96
|
+
if (star !== -1) {
|
|
97
|
+
const list = dig(json, dotted.slice(0, star));
|
|
98
|
+
return Array.isArray(list) ? list.map((item) => dig(item, dotted.slice(star + 3))) : undefined;
|
|
99
|
+
}
|
|
100
|
+
let current: unknown = json;
|
|
101
|
+
for (const key of dotted.split('.')) {
|
|
102
|
+
if (Array.isArray(current)) current = current[Number(key)];
|
|
103
|
+
else if (current && typeof current === 'object') current = (current as Record<string, unknown>)[key];
|
|
104
|
+
else return undefined;
|
|
105
|
+
}
|
|
106
|
+
return current;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
|
|
110
|
+
const random = (kind: string): string | undefined => {
|
|
111
|
+
if (kind === 'letters') return Array.from({ length: 6 }, () => LETTERS[Math.floor(Math.random() * 26)]).join('');
|
|
112
|
+
if (kind === 'digits') return Array.from({ length: 6 }, () => Math.floor(Math.random() * 10)).join('');
|
|
113
|
+
return undefined;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type Scope = Record<string, unknown>;
|
|
117
|
+
|
|
118
|
+
/** `{{alias.path}}` from what earlier steps answered, `{{random.letters}}`, `{{env.NAME}}`. */
|
|
119
|
+
export const interpolate = <T>(value: T, scope: Scope): T => {
|
|
120
|
+
if (typeof value === 'string') {
|
|
121
|
+
const whole = /^\{\{\s*([\w-]+)\.([\w.*-]+)\s*\}\}$/.exec(value);
|
|
122
|
+
const lookup = (name: string, path: string): unknown => {
|
|
123
|
+
if (name === 'random') return random(path);
|
|
124
|
+
if (name === 'env') return process.env[path];
|
|
125
|
+
if (!(name in scope)) throw new Error(`no alias '${name}' (add \`as: ${name}\` to the step that creates it)`);
|
|
126
|
+
return dig(scope[name], path);
|
|
127
|
+
};
|
|
128
|
+
// A value that is one reference keeps its type; inside a sentence it is text.
|
|
129
|
+
if (whole) {
|
|
130
|
+
const found = lookup(whole[1]!, whole[2]!);
|
|
131
|
+
if (found === undefined || found === null) throw new Error(`${value} is empty`);
|
|
132
|
+
return found as T;
|
|
133
|
+
}
|
|
134
|
+
return value.replace(/\{\{\s*([\w-]+)\.([\w.*-]+)\s*\}\}/g, (ref, name: string, path: string) => {
|
|
135
|
+
const found = lookup(name, path);
|
|
136
|
+
if (found === undefined || found === null) throw new Error(`${ref} is empty`);
|
|
137
|
+
return typeof found === 'object' ? JSON.stringify(found) : String(found);
|
|
138
|
+
}) as T;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(value)) return value.map((v) => interpolate(v, scope)) as T;
|
|
141
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, interpolate(v, scope)])) as T;
|
|
142
|
+
return value;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type Matcher = {
|
|
146
|
+
equals?: unknown;
|
|
147
|
+
near?: unknown;
|
|
148
|
+
gte?: unknown;
|
|
149
|
+
lte?: unknown;
|
|
150
|
+
present?: boolean;
|
|
151
|
+
absent?: boolean;
|
|
152
|
+
includes?: unknown;
|
|
153
|
+
matches?: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const MATCHERS = ['equals', 'near', 'gte', 'lte', 'present', 'absent', 'includes', 'matches'] as const;
|
|
157
|
+
|
|
158
|
+
export const matcherOf = (params: Record<string, unknown>): Matcher | undefined => {
|
|
159
|
+
const found = MATCHERS.filter((m) => m in params);
|
|
160
|
+
return found.length ? (Object.fromEntries(found.map((m) => [m, params[m]])) as Matcher) : undefined;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const num = (v: unknown): number | undefined => {
|
|
164
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : Number.NaN;
|
|
165
|
+
return Number.isFinite(n) ? n : undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Whether a value read from the backend is what the case said it would be. */
|
|
169
|
+
export const matches = (got: unknown, m: Matcher): boolean => {
|
|
170
|
+
if ('equals' in m) {
|
|
171
|
+
const a = num(got);
|
|
172
|
+
const b = num(m.equals);
|
|
173
|
+
return a !== undefined && b !== undefined ? a === b : String(got) === String(m.equals);
|
|
174
|
+
}
|
|
175
|
+
if ('near' in m) return num(got) !== undefined && num(m.near) !== undefined && Math.abs(num(got)! - num(m.near)!) <= 0.01;
|
|
176
|
+
if ('gte' in m) return num(got) !== undefined && num(got)! >= (num(m.gte) ?? Number.NaN);
|
|
177
|
+
if ('lte' in m) return num(got) !== undefined && num(got)! <= (num(m.lte) ?? Number.NaN);
|
|
178
|
+
if ('present' in m) return m.present ? got !== undefined && got !== null : got === undefined || got === null;
|
|
179
|
+
if ('absent' in m) return m.absent ? got === undefined || got === null : got !== undefined && got !== null;
|
|
180
|
+
if ('includes' in m) return Array.isArray(got) ? got.some((v) => String(v).includes(String(m.includes))) : String(got ?? '').includes(String(m.includes));
|
|
181
|
+
if ('matches' in m) return new RegExp(m.matches!).test(String(got ?? ''));
|
|
182
|
+
return false;
|
|
183
|
+
};
|