@icyouo/evt-cli 0.1.0 → 0.1.2

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.
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: evt-api-scanner
3
+ description: Use when an agent needs to discover API source locations, generate evt-cli scanner config, scan a codebase for HTTP endpoints, sync YAML API definitions, check endpoint coverage, or bootstrap endpoint data for evt-cli projects.
4
+ ---
5
+
6
+ # evt-api-scanner
7
+
8
+ Use this skill when working with an evt-cli project and you need to discover,
9
+ scan, sync, or audit YAML API definitions.
10
+
11
+ ## Workflow
12
+
13
+ 1. Locate the project root and evt config root.
14
+ - Prefer the user's explicit `--config-root`.
15
+ - Otherwise use `EVT_CLI_ROOT`.
16
+ - Otherwise use `./cli` from the project root.
17
+ 2. Read any existing non-example API YAML first. Preserve existing endpoint IDs,
18
+ service names, auth flags, schema, response, and dangerous markers unless
19
+ source evidence proves they are wrong.
20
+ 3. Discover source locations and write scanner config:
21
+
22
+ ```bash
23
+ node skills/evt-api-scanner/scripts/discover.js --root . --config-root ./cli
24
+ ```
25
+
26
+ 4. Generate or update endpoint YAML:
27
+
28
+ ```bash
29
+ evt api sync --config-root ./cli
30
+ ```
31
+
32
+ 5. Enrich the generated YAML from source. Do not stop at scanner skeletons.
33
+ 6. Audit YAML quality:
34
+
35
+ ```bash
36
+ evt api audit --config-root ./cli --strict
37
+ ```
38
+
39
+ If strict audit fails because of empty schemas, generic responses, unresolved
40
+ paths, or unmatched services, use the audit samples as the next edit queue and
41
+ continue enrichment.
42
+
43
+ 7. Check YAML coverage:
44
+
45
+ ```bash
46
+ evt api coverage --config-root ./cli
47
+ ```
48
+
49
+ 8. When bootstrapping an empty project, continue with:
50
+ - `evt-profile-generator` to create `data/profiles/*.json`.
51
+ - `evt-flow-generator` to create `data/flows/*.yaml`.
52
+
53
+ ## Source Evidence
54
+
55
+ Collect these signals before finalizing YAML:
56
+
57
+ - HTTP service declarations for method, path, body type, and function names.
58
+ - Repository or client call sites for auth requirements, default arguments,
59
+ endpoint aliases, and feature ownership.
60
+ - Request DTOs, method parameters, annotations, or typed request builders for
61
+ `schema.path`, `schema.query`, and `schema.body`.
62
+ - Response DTOs, generic wrappers, serializers, or model declarations for
63
+ `response.envelope`, `response.data.model`, `response.data.type`, and fields.
64
+ - App config, DI modules, HTTP clients, interceptors, or base URL providers for
65
+ `service` names and absolute URL handling.
66
+ - Existing tests, docs, and product flows for dangerous operations and required
67
+ ordering.
68
+
69
+ For Kotlin/KMP projects, inspect service interfaces, repositories, DTO/model
70
+ files, app config, DI modules, and header/interceptor builders. For Swift, JS,
71
+ and Dart projects, inspect equivalent API clients, request models, response
72
+ models, environment config, and auth/header middleware.
73
+
74
+ ## Endpoint Format
75
+
76
+ Endpoint definitions are always persisted as YAML under `data/apis/*.yaml` or
77
+ legacy `apis/*.yaml`. JSON is only used as a machine-readable intermediate
78
+ between scanner scripts and evt-cli.
79
+
80
+ Example YAML:
81
+
82
+ ```yaml
83
+ namespace: auth
84
+
85
+ endpoints:
86
+ login:
87
+ method: "POST"
88
+ path: "/api/login"
89
+ auth: false
90
+ schema:
91
+ body:
92
+ email:
93
+ type: "string"
94
+ required: true
95
+ response:
96
+ envelope: "Resp"
97
+ data:
98
+ type: "object"
99
+ model: "LoginData"
100
+ fields:
101
+ token: "string"
102
+ user:
103
+ type: "object"
104
+ ```
105
+
106
+ ## Quality Rules
107
+
108
+ - Endpoint IDs should be stable. Preserve existing IDs. If no YAML exists, use
109
+ source function names exactly and do not add, remove, or normalize prefixes
110
+ such as `get` unless a project convention proves that mapping.
111
+ - Do not leave `schema: {}` when source parameters or request DTOs exist.
112
+ - Do not use generic `response.data.type: object` when a response type or DTO
113
+ can be resolved.
114
+ - For wrapped responses such as `Resp<T>`, record the envelope and the nested
115
+ data model.
116
+ - Preserve path parameters, query parameters embedded in path strings, and
117
+ absolute URLs. Map alternate hosts to `service` when profiles can provide a
118
+ named base URL.
119
+ - Infer `auth` from the HTTP client, repository call path, interceptor, or
120
+ feature state. Public market/config endpoints are usually unauthenticated;
121
+ user/account/order/history endpoints usually require auth. Mark uncertain
122
+ cases in the final report.
123
+ - Mark mutating or irreversible endpoints with `dangerous: true`; examples are
124
+ order creation/cancelation, withdrawal, transfer, account deletion, and
125
+ credential changes.
126
+ - Keep scanner JSON internal. Runtime endpoint data remains YAML.
127
+
128
+ ## Completion Check
129
+
130
+ Run these before final answer:
131
+
132
+ ```bash
133
+ evt validate --config-root ./cli
134
+ evt api audit --config-root ./cli --strict
135
+ evt api coverage --config-root ./cli
136
+ ```
137
+
138
+ Report endpoint count, empty-schema count, generic-response count, auth
139
+ uncertain count, and any endpoints whose ID or path could not be resolved
140
+ confidently.
141
+
142
+ ## Fallback
143
+
144
+ evt-cli should prefer this skill scanner when configured. If the skill scanner
145
+ fails or returns no endpoints, evt-cli falls back to the built-in regex scanner
146
+ targets. The fallback exists for users without an agent or skill-aware workflow,
147
+ but it is less complete.
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const IGNORE_DIRS = new Set([
7
+ ".git",
8
+ ".gradle",
9
+ ".idea",
10
+ ".next",
11
+ ".nuxt",
12
+ ".svelte-kit",
13
+ ".ufoo",
14
+ "build",
15
+ "coverage",
16
+ "dist",
17
+ "node_modules",
18
+ "Pods",
19
+ "target"
20
+ ]);
21
+
22
+ const LANGUAGE_DEFS = {
23
+ kotlin: {
24
+ extensions: [".kt"],
25
+ hints: [/Service\.kt$/i, /Api\.kt$/i, /Repository\.kt$/i, /http\.(get|post|put|patch|delete)/]
26
+ },
27
+ swift: {
28
+ extensions: [".swift"],
29
+ hints: [/Service\.swift$/i, /API\.swift$/i, /Api\.swift$/i, /client\.(get|post|put|patch|delete)/]
30
+ },
31
+ js: {
32
+ extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
33
+ hints: [/(service|api|client|request|http)\.[cm]?[jt]sx?$/i, /\b(fetch|axios)\s*\(/, /\.(get|post|put|patch|delete)\s*\(/]
34
+ },
35
+ dart: {
36
+ extensions: [".dart"],
37
+ hints: [/(service|api|client|repository)\.dart$/i, /\.(get|post|put|patch|delete)\s*\(/]
38
+ }
39
+ };
40
+
41
+ function parseArgs(argv) {
42
+ const options = {};
43
+ for (let index = 0; index < argv.length; index += 1) {
44
+ const token = argv[index];
45
+ if (!token.startsWith("--")) continue;
46
+ const raw = token.slice(2);
47
+ const equals = raw.indexOf("=");
48
+ const key = (equals >= 0 ? raw.slice(0, equals) : raw)
49
+ .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
50
+ if (key === "dryRun" || key === "json") {
51
+ options[key] = true;
52
+ } else {
53
+ options[key] = equals >= 0 ? raw.slice(equals + 1) : argv[++index];
54
+ }
55
+ }
56
+ return options;
57
+ }
58
+
59
+ function posixRelative(from, to) {
60
+ return path.relative(from, to).split(path.sep).join("/") || ".";
61
+ }
62
+
63
+ function defaultConfigRoot(cwd) {
64
+ if (process.env.EVT_CLI_ROOT) return path.resolve(process.env.EVT_CLI_ROOT);
65
+ if (process.env.EVERYTHING_CLI_ROOT) return path.resolve(process.env.EVERYTHING_CLI_ROOT);
66
+ if (path.basename(cwd) === "cli") return cwd;
67
+ return path.join(cwd, "cli");
68
+ }
69
+
70
+ function walkFiles(root, files = []) {
71
+ if (!fs.existsSync(root)) return files;
72
+ const stat = fs.statSync(root);
73
+ if (stat.isFile()) {
74
+ files.push(root);
75
+ return files;
76
+ }
77
+ if (!stat.isDirectory()) return files;
78
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
79
+ if (entry.isDirectory() && IGNORE_DIRS.has(entry.name)) continue;
80
+ walkFiles(path.join(root, entry.name), files);
81
+ }
82
+ return files;
83
+ }
84
+
85
+ function fileLooksRelevant(file, language, root) {
86
+ const def = LANGUAGE_DEFS[language];
87
+ const relative = posixRelative(root, file);
88
+ const basename = path.basename(file);
89
+ const text = fs.readFileSync(file, "utf8").slice(0, 20000);
90
+ return def.hints.some((hint) => hint.test(basename) || hint.test(relative) || hint.test(text));
91
+ }
92
+
93
+ function collapseDirs(root, files) {
94
+ const dirs = [...new Set(files.map((file) => path.dirname(file)))]
95
+ .map((dir) => posixRelative(root, dir))
96
+ .sort((left, right) => left.length - right.length || left.localeCompare(right));
97
+ const collapsed = [];
98
+ for (const dir of dirs) {
99
+ if (!collapsed.some((parent) => dir === parent || dir.startsWith(`${parent}/`))) {
100
+ collapsed.push(dir);
101
+ }
102
+ }
103
+ return collapsed;
104
+ }
105
+
106
+ function discoverTargets(projectRoot) {
107
+ const files = walkFiles(projectRoot);
108
+ const targets = [];
109
+ for (const [language, def] of Object.entries(LANGUAGE_DEFS)) {
110
+ const matching = files
111
+ .filter((file) => def.extensions.includes(path.extname(file)))
112
+ .filter((file) => fileLooksRelevant(file, language, projectRoot));
113
+ if (matching.length === 0) continue;
114
+ targets.push({ language, paths: collapseDirs(projectRoot, matching) });
115
+ }
116
+ return targets;
117
+ }
118
+
119
+ function main() {
120
+ const options = parseArgs(process.argv.slice(2));
121
+ const cwd = process.cwd();
122
+ const configRoot = path.resolve(options.configRoot || defaultConfigRoot(cwd));
123
+ const projectRoot = path.resolve(options.root || (path.basename(configRoot) === "cli" ? path.dirname(configRoot) : cwd));
124
+ const dataDir = path.join(configRoot, "data");
125
+ const apiDir = path.join(dataDir, "apis");
126
+ const scannerFile = path.resolve(options.scannerFile || path.join(dataDir, "scanner.json"));
127
+ const scannerDir = path.dirname(scannerFile);
128
+ const fallbackTargets = discoverTargets(projectRoot);
129
+ const config = {
130
+ root: posixRelative(scannerDir, projectRoot),
131
+ apiDir: posixRelative(scannerDir, apiDir),
132
+ targets: [
133
+ {
134
+ language: "skill",
135
+ name: "evt-api-scanner",
136
+ skill: "evt-api-scanner"
137
+ },
138
+ ...fallbackTargets
139
+ ]
140
+ };
141
+
142
+ if (!options.dryRun) {
143
+ fs.mkdirSync(apiDir, { recursive: true });
144
+ fs.mkdirSync(path.join(dataDir, "flows"), { recursive: true });
145
+ fs.mkdirSync(path.join(dataDir, "profiles"), { recursive: true });
146
+ fs.mkdirSync(scannerDir, { recursive: true });
147
+ fs.writeFileSync(scannerFile, `${JSON.stringify(config, null, 2)}\n`);
148
+ }
149
+
150
+ process.stdout.write(`${JSON.stringify({
151
+ wrote: !options.dryRun,
152
+ projectRoot,
153
+ configRoot,
154
+ scannerFile,
155
+ apiDir,
156
+ targets: config.targets
157
+ }, null, 2)}\n`);
158
+ }
159
+
160
+ main();
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ const IGNORE_DIRS = new Set([
7
+ ".git",
8
+ ".gradle",
9
+ ".idea",
10
+ ".next",
11
+ ".nuxt",
12
+ ".svelte-kit",
13
+ ".ufoo",
14
+ "build",
15
+ "coverage",
16
+ "dist",
17
+ "node_modules",
18
+ "Pods",
19
+ "target"
20
+ ]);
21
+
22
+ const LANGUAGE_DEFS = {
23
+ kotlin: {
24
+ extensions: [".kt"],
25
+ receivers: ["http", "client", "api"],
26
+ helpers: ["get", "getRaw", "post", "postUrlEncoded", "postForm", "put", "patch", "delete", "deleteWithBody", "upload"],
27
+ functions: [/(?:override\s+)?suspend\s+fun\s+([A-Za-z_][A-Za-z0-9_]*)/g, /fun\s+([A-Za-z_][A-Za-z0-9_]*)/g],
28
+ stripPrefixes: ["I"],
29
+ stripSuffixes: ["Service", "Api", "API", "Repository"]
30
+ },
31
+ swift: {
32
+ extensions: [".swift"],
33
+ receivers: ["http", "client", "api"],
34
+ helpers: ["get", "post", "put", "patch", "delete", "upload"],
35
+ functions: [/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g],
36
+ stripSuffixes: ["Service", "Api", "API"]
37
+ },
38
+ js: {
39
+ extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
40
+ receivers: ["http", "client", "api", "axios"],
41
+ helpers: ["get", "post", "put", "patch", "delete"],
42
+ functions: [
43
+ /(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g,
44
+ /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\(/g,
45
+ /([A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*(?:async\s*)?function\s*\(/g
46
+ ],
47
+ stripSuffixes: ["Service", "Api", "API"]
48
+ },
49
+ dart: {
50
+ extensions: [".dart"],
51
+ receivers: ["http", "client", "api", "dio"],
52
+ helpers: ["get", "post", "put", "patch", "delete"],
53
+ functions: [
54
+ /(?:Future(?:<[^>]+>)?|Stream(?:<[^>]+>)?|void|[A-Za-z_][A-Za-z0-9_<>?]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*\)\s*(?:async\s*)?\{/g
55
+ ],
56
+ stripSuffixes: ["Service", "Api", "API"]
57
+ }
58
+ };
59
+
60
+ const METHOD_BY_HELPER = {
61
+ get: "GET",
62
+ getRaw: "GET",
63
+ fetch: "GET",
64
+ post: "POST",
65
+ postForm: "POST",
66
+ postUrlEncoded: "POST",
67
+ put: "PUT",
68
+ patch: "PATCH",
69
+ delete: "DELETE",
70
+ deleteWithBody: "DELETE",
71
+ upload: "POST"
72
+ };
73
+
74
+ const STRING_PATTERN = `"([^"]+)"|'([^']+)'|\`([^\`]+)\``;
75
+
76
+ function parseArgs(argv) {
77
+ const options = { paths: [] };
78
+ for (let index = 0; index < argv.length; index += 1) {
79
+ const token = argv[index];
80
+ if (!token.startsWith("--")) {
81
+ options.paths.push(token);
82
+ continue;
83
+ }
84
+ const raw = token.slice(2);
85
+ const equals = raw.indexOf("=");
86
+ const key = (equals >= 0 ? raw.slice(0, equals) : raw)
87
+ .replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
88
+ options[key] = equals >= 0 ? raw.slice(equals + 1) : argv[++index];
89
+ }
90
+ return options;
91
+ }
92
+
93
+ function posixRelative(from, to) {
94
+ return path.relative(from, to).split(path.sep).join("/") || ".";
95
+ }
96
+
97
+ function walkFiles(root, files = []) {
98
+ if (!fs.existsSync(root)) return files;
99
+ const stat = fs.statSync(root);
100
+ if (stat.isFile()) {
101
+ files.push(root);
102
+ return files;
103
+ }
104
+ if (!stat.isDirectory()) return files;
105
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
106
+ if (entry.isDirectory() && IGNORE_DIRS.has(entry.name)) continue;
107
+ walkFiles(path.join(root, entry.name), files);
108
+ }
109
+ return files;
110
+ }
111
+
112
+ function languageForFile(file) {
113
+ const extension = path.extname(file);
114
+ return Object.entries(LANGUAGE_DEFS).find(([, def]) => def.extensions.includes(extension))?.[0];
115
+ }
116
+
117
+ function makeRegexUnion(values) {
118
+ return values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
119
+ }
120
+
121
+ function firstQuoted(match, startIndex) {
122
+ for (let index = startIndex; index < match.length; index += 1) {
123
+ if (match[index] !== undefined) return match[index];
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ function nearestFunction(source, index, def) {
129
+ const before = source.slice(0, index);
130
+ let nearest = { index: -1, name: "unknown" };
131
+ for (const pattern of def.functions) {
132
+ pattern.lastIndex = 0;
133
+ for (const match of before.matchAll(pattern)) {
134
+ if (match.index > nearest.index) nearest = { index: match.index, name: match[1] };
135
+ }
136
+ }
137
+ return nearest.name;
138
+ }
139
+
140
+ function namespaceForFile(file, language) {
141
+ const def = LANGUAGE_DEFS[language];
142
+ let namespace = path.basename(file, path.extname(file));
143
+ for (const prefix of def.stripPrefixes || []) {
144
+ if (namespace.startsWith(prefix)) namespace = namespace.slice(prefix.length);
145
+ }
146
+ for (const suffix of def.stripSuffixes || []) {
147
+ if (namespace.endsWith(suffix)) namespace = namespace.slice(0, -suffix.length);
148
+ }
149
+ return namespace.replace(/^[A-Z]/, (letter) => letter.toLowerCase());
150
+ }
151
+
152
+ function normalizePath(rawPath, language) {
153
+ if (!rawPath || !rawPath.startsWith("/")) return undefined;
154
+ let value = rawPath
155
+ .replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, ":$1")
156
+ .replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, ":$1");
157
+ if (language === "swift") value = value.replace(/\\\(([A-Za-z_][A-Za-z0-9_]*)\)/g, ":$1");
158
+ return value;
159
+ }
160
+
161
+ function bodyTypeFor(helper) {
162
+ if (helper === "postUrlEncoded") return "formUrlEncoded";
163
+ if (helper === "postForm" || helper === "upload") return "multipart";
164
+ return "json";
165
+ }
166
+
167
+ function pushEndpoint(endpoints, seen, file, root, source, language, helper, rawPath, matchIndex, explicitMethod) {
168
+ const pathTemplate = normalizePath(rawPath, language);
169
+ if (!pathTemplate) return;
170
+ const namespace = namespaceForFile(file, language);
171
+ const functionName = nearestFunction(source, matchIndex, LANGUAGE_DEFS[language]);
172
+ const method = String(explicitMethod || METHOD_BY_HELPER[helper] || helper).toUpperCase();
173
+ const key = `${method}:${pathTemplate}:${functionName}`;
174
+ if (seen.has(key)) return;
175
+ seen.add(key);
176
+ endpoints.push({
177
+ id: `${namespace}.${functionName}`,
178
+ namespace,
179
+ functionName,
180
+ method,
181
+ path: pathTemplate,
182
+ bodyType: bodyTypeFor(helper),
183
+ helper,
184
+ file: posixRelative(root, file),
185
+ source: "evt-api-scanner"
186
+ });
187
+ }
188
+
189
+ function scanFile(file, root) {
190
+ const language = languageForFile(file);
191
+ if (!language) return [];
192
+ const def = LANGUAGE_DEFS[language];
193
+ const source = fs.readFileSync(file, "utf8");
194
+ const endpoints = [];
195
+ const seen = new Set();
196
+ const receiverPattern = makeRegexUnion(def.receivers);
197
+ const helperPattern = makeRegexUnion(def.helpers);
198
+ const calls = [
199
+ new RegExp(`\\b(?:${receiverPattern})\\s*\\.\\s*(${helperPattern})(?:<[^>]+>)?\\s*\\(\\s*(?:${STRING_PATTERN})`, "g"),
200
+ new RegExp(`\\b(?:${receiverPattern})\\s*\\.\\s*(${helperPattern})(?:<[^>]+>)?\\s*\\([\\s\\S]{0,300}?\\burl\\s*=\\s*(?:${STRING_PATTERN})`, "g"),
201
+ new RegExp(`\\b(?:${receiverPattern})\\s*\\.\\s*(${helperPattern})\\s*\\(\\s*Uri\\.parse\\s*\\(\\s*(?:${STRING_PATTERN})`, "g")
202
+ ];
203
+ for (const pattern of calls) {
204
+ let match;
205
+ while ((match = pattern.exec(source)) !== null) {
206
+ pushEndpoint(endpoints, seen, file, root, source, language, match[1], firstQuoted(match, 2), match.index);
207
+ }
208
+ }
209
+
210
+ if (language === "js") {
211
+ const fetchCall = new RegExp(`\\bfetch\\s*\\(\\s*(?:${STRING_PATTERN})([\\s\\S]{0,300}?)\\)`, "g");
212
+ let match;
213
+ while ((match = fetchCall.exec(source)) !== null) {
214
+ const optionsSource = match[4] || "";
215
+ const methodMatch = optionsSource.match(/\bmethod\s*:\s*["'`]([A-Za-z]+)["'`]/);
216
+ pushEndpoint(endpoints, seen, file, root, source, language, "fetch", firstQuoted(match, 1), match.index, methodMatch && methodMatch[1]);
217
+ }
218
+ }
219
+ return endpoints;
220
+ }
221
+
222
+ function main() {
223
+ const options = parseArgs(process.argv.slice(2));
224
+ const root = path.resolve(options.root || process.env.EVT_SCAN_ROOT || process.cwd());
225
+ const entries = options.paths.length > 0 ? options.paths.map((entry) => path.resolve(root, entry)) : [root];
226
+ const files = entries.flatMap((entry) => walkFiles(entry)).filter(languageForFile);
227
+ const endpoints = files.flatMap((file) => scanFile(file, root));
228
+ process.stdout.write(`${JSON.stringify({ endpoints }, null, 2)}\n`);
229
+ }
230
+
231
+ main();
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: evt-flow-generator
3
+ description: Use when an agent needs to create or update evt-cli YAML flows from API YAML definitions, product workflows, authentication requirements, or test scenarios, including interactive inputs, waits, token cache saves, and feature smoke flows.
4
+ ---
5
+
6
+ # evt-flow-generator
7
+
8
+ Use this skill to produce executable evt-cli flows under `data/flows/*.yaml` or
9
+ legacy `flows/*.yaml`.
10
+
11
+ ## Workflow
12
+
13
+ 1. Locate the evt config root.
14
+ - Prefer the user's explicit `--config-root`.
15
+ - Otherwise use `EVT_CLI_ROOT`.
16
+ - Otherwise use `./cli` from the project root.
17
+ 2. Inspect available endpoints:
18
+
19
+ ```bash
20
+ evt api list --config-root ./cli
21
+ evt api show <namespace.endpoint> --config-root ./cli
22
+ ```
23
+
24
+ 3. Read existing flow examples and product docs/source to identify:
25
+ - login or session refresh sequence
26
+ - token field path in the login response
27
+ - prerequisite calls for feature flows
28
+ - required waits between calls
29
+ - safe smoke-test values from profile fixtures
30
+ 4. Read the evt runtime semantics before saving values:
31
+ - each step result contains `response`, the full parsed response
32
+ - each step result contains `data`, the unwrapped `response.data` when present
33
+ - for wrapped APIs, prefer `steps.<id>.response.data.<field>` for cache saves
34
+ 5. Write YAML flows such as:
35
+ - `data/flows/login.yaml`
36
+ - `data/flows/smoke.yaml`
37
+ - `data/flows/<feature>.yaml`
38
+ 6. Validate:
39
+
40
+ ```bash
41
+ evt validate --config-root ./cli
42
+ ```
43
+
44
+ ## Flow Format
45
+
46
+ ```yaml
47
+ name: login
48
+ description: Login and save session token.
49
+
50
+ inputs:
51
+ email:
52
+ prompt: Email
53
+ type: string
54
+ required: true
55
+ password:
56
+ prompt: Password
57
+ type: password
58
+ required: true
59
+
60
+ steps:
61
+ - id: login
62
+ call: auth.login
63
+ body:
64
+ email: "{{inputs.email}}"
65
+ password: "{{inputs.password}}"
66
+ expect:
67
+ path: response.data.token
68
+ exists: true
69
+
70
+ save:
71
+ cache:
72
+ token: "{{steps.login.response.data.token}}"
73
+ required:
74
+ - cache.token
75
+ ```
76
+
77
+ Use waits when the backend needs spacing:
78
+
79
+ ```yaml
80
+ steps:
81
+ - id: createOrder
82
+ call: trade.createOrder
83
+ body:
84
+ symbol: "{{inputs.symbol}}"
85
+ side: "BUY"
86
+ - wait: 2s
87
+ - id: queryOrder
88
+ call: trade.orderDetail
89
+ query:
90
+ orderId: "{{steps.createOrder.response.data.orderId}}"
91
+ ```
92
+
93
+ Use `afterWait` when only one step needs a post-call interval.
94
+
95
+ ## Rules
96
+
97
+ - Runtime flows are YAML. Do not persist flow definitions as JSON.
98
+ - Keep bundled `*.example.yaml` generic; write project-specific flows to
99
+ non-example YAML files.
100
+ - Use `inputs` for values a user may need to type dynamically.
101
+ - Use `profile.inputs` by naming flow inputs the same way as profile keys.
102
+ - Use profile-backed values for device/session fields when source does so, for
103
+ example `{{profile.device.fingerprintId}}` instead of a new placeholder input.
104
+ - Use `wait` or `afterWait` for backend intervals; valid values include `500ms`,
105
+ `2s`, and `1m`.
106
+ - Preserve waits found in source, tests, docs, or existing flows.
107
+ - Save login state through `save.cache.token` and `save.required`. For wrapped
108
+ responses, prefer `{{steps.login.response.data.token}}`.
109
+ - Reference previous step outputs with `{{steps.<id>.<path>}}`.
110
+ - Prefer `expect` checks for required response fields when a flow depends on
111
+ them.
112
+ - Put logout last in flows that include logout.
113
+ - For feature flows, include prerequisite reads, state-changing calls, follow-up
114
+ reads, waits, cleanup, and final assertions when the scenario needs them.
115
+ - Use endpoint schema and profile fixtures to build request bodies. Do not
116
+ invent field names that are absent from endpoint YAML or source models.
117
+ - Do not include destructive business actions unless the project or user states
118
+ that the environment is safe for testing.
119
+
120
+ ## Completion Check
121
+
122
+ After writing flows, run:
123
+
124
+ ```bash
125
+ evt validate --config-root ./cli
126
+ evt flow run login --config-root ./cli --profile <profile> --dry-run
127
+ ```
128
+
129
+ For login flows, inspect the dry-run output and verify:
130
+
131
+ - the expected endpoint order is present
132
+ - required waits are present
133
+ - token save path matches the response model
134
+ - headers come from the selected profile
135
+ - no required input is missing
136
+
137
+ When real credentials and a safe test environment are available, run the login
138
+ flow without `--dry-run`, confirm the token is written to cache, then run the
139
+ authenticated smoke or feature flow. Report any step that was only inferred.