@docsxai/engine 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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/dist/auth/api-login.d.ts +69 -0
  4. package/dist/auth/api-login.js +95 -0
  5. package/dist/auth/browser-session.d.ts +28 -0
  6. package/dist/auth/browser-session.js +43 -0
  7. package/dist/auth/cookie-jar.d.ts +58 -0
  8. package/dist/auth/cookie-jar.js +212 -0
  9. package/dist/auth/email-otp.d.ts +210 -0
  10. package/dist/auth/email-otp.js +166 -0
  11. package/dist/auth/http-basic.d.ts +5 -0
  12. package/dist/auth/http-basic.js +17 -0
  13. package/dist/auth/index.d.ts +47 -0
  14. package/dist/auth/index.js +137 -0
  15. package/dist/auth/jwt-injection.d.ts +153 -0
  16. package/dist/auth/jwt-injection.js +136 -0
  17. package/dist/auth/manual-capture.d.ts +35 -0
  18. package/dist/auth/manual-capture.js +30 -0
  19. package/dist/auth/mtls.d.ts +15 -0
  20. package/dist/auth/mtls.js +53 -0
  21. package/dist/auth/pat-header.d.ts +19 -0
  22. package/dist/auth/pat-header.js +34 -0
  23. package/dist/auth/storage-state-cache.d.ts +38 -0
  24. package/dist/auth/storage-state-cache.js +143 -0
  25. package/dist/auth/test-backdoor.d.ts +25 -0
  26. package/dist/auth/test-backdoor.js +51 -0
  27. package/dist/auth/totp.d.ts +39 -0
  28. package/dist/auth/totp.js +108 -0
  29. package/dist/auth/types.d.ts +86 -0
  30. package/dist/auth/types.js +57 -0
  31. package/dist/auth/ui-form.d.ts +204 -0
  32. package/dist/auth/ui-form.js +153 -0
  33. package/dist/auth/webauthn.d.ts +88 -0
  34. package/dist/auth/webauthn.js +67 -0
  35. package/dist/auth.d.ts +1 -0
  36. package/dist/auth.js +3 -0
  37. package/dist/backend-client-contracts.d.ts +88 -0
  38. package/dist/backend-client-contracts.js +19 -0
  39. package/dist/backend-client-oauth-login.d.ts +7 -0
  40. package/dist/backend-client-oauth-login.js +90 -0
  41. package/dist/backend-client-state-cache.d.ts +73 -0
  42. package/dist/backend-client-state-cache.js +185 -0
  43. package/dist/backend-client-token.d.ts +18 -0
  44. package/dist/backend-client-token.js +94 -0
  45. package/dist/backend-client-transport.d.ts +66 -0
  46. package/dist/backend-client-transport.js +181 -0
  47. package/dist/backend-client.d.ts +5 -0
  48. package/dist/backend-client.js +18 -0
  49. package/dist/calibrate.d.ts +31 -0
  50. package/dist/calibrate.js +68 -0
  51. package/dist/cli-commands-authoring.d.ts +5 -0
  52. package/dist/cli-commands-authoring.js +403 -0
  53. package/dist/cli-commands-backend.d.ts +5 -0
  54. package/dist/cli-commands-backend.js +211 -0
  55. package/dist/cli-commands-docpack.d.ts +5 -0
  56. package/dist/cli-commands-docpack.js +280 -0
  57. package/dist/cli-commands-session.d.ts +4 -0
  58. package/dist/cli-commands-session.js +398 -0
  59. package/dist/cli-shared.d.ts +5 -0
  60. package/dist/cli-shared.js +45 -0
  61. package/dist/cli-usage.d.ts +1 -0
  62. package/dist/cli-usage.js +137 -0
  63. package/dist/cli.d.ts +2 -0
  64. package/dist/cli.js +77 -0
  65. package/dist/diagnose.d.ts +50 -0
  66. package/dist/diagnose.js +168 -0
  67. package/dist/diff-compute.d.ts +13 -0
  68. package/dist/diff-compute.js +378 -0
  69. package/dist/diff-report.d.ts +7 -0
  70. package/dist/diff-report.js +125 -0
  71. package/dist/diff-types.d.ts +125 -0
  72. package/dist/diff-types.js +15 -0
  73. package/dist/diff.d.ts +3 -0
  74. package/dist/diff.js +16 -0
  75. package/dist/doc-pack-io.d.ts +30 -0
  76. package/dist/doc-pack-io.js +182 -0
  77. package/dist/doc-pack.d.ts +1814 -0
  78. package/dist/doc-pack.js +328 -0
  79. package/dist/doctor-checks-plugins.d.ts +2 -0
  80. package/dist/doctor-checks-plugins.js +136 -0
  81. package/dist/doctor-checks.d.ts +56 -0
  82. package/dist/doctor-checks.js +367 -0
  83. package/dist/doctor.d.ts +7 -0
  84. package/dist/doctor.js +62 -0
  85. package/dist/export/adf.d.ts +57 -0
  86. package/dist/export/adf.js +323 -0
  87. package/dist/export/playwright-test.d.ts +26 -0
  88. package/dist/export/playwright-test.js +221 -0
  89. package/dist/flow-file.d.ts +21 -0
  90. package/dist/flow-file.js +180 -0
  91. package/dist/flow-lint.d.ts +24 -0
  92. package/dist/flow-lint.js +203 -0
  93. package/dist/flow-runtime.d.ts +113 -0
  94. package/dist/flow-runtime.js +273 -0
  95. package/dist/flow-tree.d.ts +19 -0
  96. package/dist/flow-tree.js +104 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.js +31 -0
  99. package/dist/playwright-driver.d.ts +105 -0
  100. package/dist/playwright-driver.js +363 -0
  101. package/dist/playwright-instrumented-browser.d.ts +51 -0
  102. package/dist/playwright-instrumented-browser.js +189 -0
  103. package/dist/plugins/load.d.ts +22 -0
  104. package/dist/plugins/load.js +99 -0
  105. package/dist/plugins/lock.d.ts +40 -0
  106. package/dist/plugins/lock.js +122 -0
  107. package/dist/plugins/manifest.d.ts +70 -0
  108. package/dist/plugins/manifest.js +115 -0
  109. package/dist/plugins/plan.d.ts +51 -0
  110. package/dist/plugins/plan.js +279 -0
  111. package/dist/plugins/registry.d.ts +59 -0
  112. package/dist/plugins/registry.js +71 -0
  113. package/dist/plugins/runtime.d.ts +7 -0
  114. package/dist/plugins/runtime.js +27 -0
  115. package/dist/plugins/types.d.ts +58 -0
  116. package/dist/plugins/types.js +4 -0
  117. package/dist/plugins-cli.d.ts +1 -0
  118. package/dist/plugins-cli.js +191 -0
  119. package/dist/redact.d.ts +16 -0
  120. package/dist/redact.js +72 -0
  121. package/dist/style.d.ts +46 -0
  122. package/dist/style.js +151 -0
  123. package/dist/viewer-bin.d.ts +20 -0
  124. package/dist/viewer-bin.js +97 -0
  125. package/dist/workspace.d.ts +60 -0
  126. package/dist/workspace.js +172 -0
  127. package/dist/zip.d.ts +17 -0
  128. package/dist/zip.js +113 -0
  129. package/package.json +64 -0
@@ -0,0 +1,172 @@
1
+ // Workspace scaffolding & config.
2
+ //
3
+ // A *workspace* is the directory docsxai reads/writes for a project: `flows/`, `docs/`, `auth/`, plus
4
+ // (gitignored) `.auth/` and `.viewer/`, plus a small `.docsxai.json` config so `run`/`render`/`capture-auth`
5
+ // don't need `--base-url` / `--ignore-https-errors` on every call. **It must live outside the app repo** — docsxai
6
+ // operates *on* a running app from outside; it never writes into the app's source tree.
7
+ //
8
+ // `docsxai init <dir> --app-url <url>` scaffolds one; `--persist tmp` puts it in a throwaway temp dir.
9
+ import { promises as fs } from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import { stringify as stringifyYaml } from "yaml";
13
+ export const WORKSPACE_CONFIG_FILE = ".docsxai.json";
14
+ /** Thrown when a workspace-relative path resolves outside the workspace root. */
15
+ export class WorkspacePathEscapeError extends Error {
16
+ workspaceDir;
17
+ resolvedPath;
18
+ constructor(workspaceDir, resolvedPath) {
19
+ super(`path escapes workspace root ${workspaceDir}: ${resolvedPath}`);
20
+ this.workspaceDir = workspaceDir;
21
+ this.resolvedPath = resolvedPath;
22
+ this.name = "WorkspacePathEscapeError";
23
+ }
24
+ }
25
+ function assertContained(root, candidate) {
26
+ // Prefix check on a path-separator boundary: `/ws/docs-evil` must not pass for root `/ws/docs`.
27
+ if (candidate !== root && !candidate.startsWith(root + path.sep)) {
28
+ throw new WorkspacePathEscapeError(root, candidate);
29
+ }
30
+ }
31
+ /** Resolve path segments against a workspace root, guaranteeing containment. */
32
+ export function resolveWorkspacePath(workspaceDir, ...segments) {
33
+ const root = path.resolve(workspaceDir);
34
+ const resolved = path.resolve(root, ...segments);
35
+ assertContained(root, resolved);
36
+ return resolved;
37
+ }
38
+ /** Realpath of the deepest existing ancestor of `p`, with the non-existing suffix re-appended. */
39
+ async function realpathDeepestExisting(p) {
40
+ let probe = p;
41
+ let suffix = "";
42
+ for (;;) {
43
+ try {
44
+ const real = await fs.realpath(probe);
45
+ return suffix ? path.join(real, suffix) : real;
46
+ }
47
+ catch {
48
+ const parent = path.dirname(probe);
49
+ if (parent === probe)
50
+ return p; // not even the filesystem root resolves — give up lexically
51
+ suffix = suffix ? path.join(path.basename(probe), suffix) : path.basename(probe);
52
+ probe = parent;
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Like {@link resolveWorkspacePath}, but additionally defends against symlink escape: the deepest
58
+ * existing ancestor of the resolved path is realpath'd and containment re-verified against the
59
+ * realpath'd workspace root. Use before writing to a path whose segments are user-influenced
60
+ * (flow names, step ids, role names) — a symlink inside the workspace pointing outside must throw.
61
+ */
62
+ export async function resolveWorkspacePathReal(workspaceDir, ...segments) {
63
+ const resolved = resolveWorkspacePath(workspaceDir, ...segments);
64
+ const rootReal = await realpathDeepestExisting(path.resolve(workspaceDir));
65
+ const resolvedReal = await realpathDeepestExisting(resolved);
66
+ assertContained(rootReal, resolvedReal);
67
+ return resolved;
68
+ }
69
+ const README = (cfg) => `# docsxai workspace
70
+
71
+ This directory is a **docsxai workspace** — it holds the doc pack for one running app:
72
+
73
+ - \`flows/<flow>.flow.yaml\` — hand-editable flow-files (the source of truth for execution)
74
+ - \`docs/<flow>/{annotations.json, screenshots/, <step>.md}\`, \`docs/{style.yaml, locators.yaml}\` — the generated docs
75
+ - \`auth/strategy.yaml\` — how execution authenticates to the app
76
+ - \`.auth/<role>.json\` — captured session(s) (gitignored; never committed; never leaves this machine)
77
+ - \`.viewer/\` — the generated interactive viewer (gitignored)
78
+ - \`${WORKSPACE_CONFIG_FILE}\` — workspace config (\`app_url${cfg.app_url ? ` = ${cfg.app_url}` : ""}\`, \`ignore_https_errors = ${!!cfg.ignore_https_errors}\`)
79
+
80
+ **Do NOT place this directory inside the app's source repo.** docsxai documents the *running* app from outside;
81
+ keeping the workspace separate is what guarantees zero traces in the app repo.
82
+
83
+ Usage (from this directory):
84
+
85
+ \`\`\`
86
+ docsxai capture-auth . # if auth is manual-capture: opens an instrumented browser; log in, then run window.__docsxai.capture()
87
+ docsxai calibrate . --description path/to/flow.md # (when the calibration stages land) produce/refresh flow-files
88
+ docsxai run . # replay the flow-files headlessly; refresh annotations + screenshots
89
+ docsxai render . && open .viewer/index.html
90
+ \`\`\`
91
+ `;
92
+ async function isNonEmptyDir(p) {
93
+ try {
94
+ const entries = await fs.readdir(p);
95
+ return entries.length > 0;
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ /** Scaffold a workspace. Returns where it landed + what was created. */
102
+ export async function initWorkspace(opts) {
103
+ let dir;
104
+ let ephemeral = false;
105
+ if (opts.persistTmp) {
106
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), "docsxai-workspace-"));
107
+ ephemeral = true;
108
+ }
109
+ else {
110
+ if (!opts.dir)
111
+ throw new Error("init: a target directory is required (or use --persist tmp)");
112
+ dir = path.resolve(opts.dir);
113
+ if (!opts.force && (await isNonEmptyDir(dir))) {
114
+ throw new Error(`init: ${dir} exists and is not empty — pick another dir or pass --force`);
115
+ }
116
+ }
117
+ const role = opts.role ?? "editor";
118
+ const ttl = opts.ttl ?? "1h";
119
+ const trigger = opts.captureTrigger ?? "console";
120
+ const auth = opts.auth ?? "manual-capture";
121
+ const created = [];
122
+ for (const sub of ["flows", "docs", "auth", ".auth", ".viewer"]) {
123
+ await fs.mkdir(resolveWorkspacePath(dir, sub), { recursive: true });
124
+ }
125
+ created.push("flows/", "docs/", "auth/", ".auth/", ".viewer/");
126
+ await fs.writeFile(resolveWorkspacePath(dir, ".gitignore"), ".auth/\n.viewer/\n", "utf8");
127
+ created.push(".gitignore");
128
+ const cfg = {
129
+ schema: "docsxai/workspace@1",
130
+ ...(opts.appUrl ? { app_url: opts.appUrl } : {}),
131
+ ...(opts.ignoreHttpsErrors ? { ignore_https_errors: true } : {}),
132
+ created_at: new Date().toISOString(),
133
+ };
134
+ await fs.writeFile(resolveWorkspacePath(dir, WORKSPACE_CONFIG_FILE), JSON.stringify(cfg, null, 2) + "\n", "utf8");
135
+ created.push(WORKSPACE_CONFIG_FILE);
136
+ if (auth === "manual-capture") {
137
+ const descriptor = {
138
+ schema: "docsxai/auth-strategy@1",
139
+ default_role: role,
140
+ roles: {
141
+ [role]: {
142
+ strategy: "manual-capture",
143
+ options: { capture_trigger: trigger },
144
+ cache: {
145
+ enabled: true,
146
+ store: "local",
147
+ ttl,
148
+ ...(opts.authCookie ? { auth_cookie: opts.authCookie } : {}),
149
+ },
150
+ },
151
+ },
152
+ };
153
+ await fs.writeFile(resolveWorkspacePath(dir, "auth", "strategy.yaml"), stringifyYaml(descriptor, { lineWidth: 100 }), "utf8");
154
+ created.push("auth/strategy.yaml");
155
+ }
156
+ await fs.writeFile(resolveWorkspacePath(dir, "README.md"), README(cfg), "utf8");
157
+ created.push("README.md");
158
+ return { dir, created, ephemeral };
159
+ }
160
+ /** Read `<dir>/.docsxai.json` if present. Returns `null` if absent or unreadable. */
161
+ export async function loadWorkspaceConfig(dir) {
162
+ try {
163
+ const text = await fs.readFile(resolveWorkspacePath(dir, WORKSPACE_CONFIG_FILE), "utf8");
164
+ const parsed = JSON.parse(text);
165
+ if (parsed && parsed.schema === "docsxai/workspace@1")
166
+ return parsed;
167
+ return null;
168
+ }
169
+ catch {
170
+ return null;
171
+ }
172
+ }
package/dist/zip.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export interface ZipOptions {
2
+ workspace: string;
3
+ output: string;
4
+ /** Include the rendered viewer (`.viewer/`) in the archive. Default false — viewers are derived from the doc pack and re-rendering is cheap. */
5
+ includeViewer?: boolean;
6
+ }
7
+ export interface ZipResult {
8
+ output: string;
9
+ bytes: number;
10
+ /** Workspace-relative archive entries, in the (sorted) order they appear in the zip. */
11
+ entries: string[];
12
+ }
13
+ export declare class ZipError extends Error {
14
+ readonly cause?: unknown | undefined;
15
+ constructor(message: string, cause?: unknown | undefined);
16
+ }
17
+ export declare function zipDocPack(opts: ZipOptions): Promise<ZipResult>;
package/dist/zip.js ADDED
@@ -0,0 +1,113 @@
1
+ // Package a workspace's doc pack into a single archive for hand-off. Zips in-process (fflate) —
2
+ // no system `zip` binary required. Excludes operator-local state (`.auth/`, halt screenshots, the
3
+ // rendered `.viewer/` by default) so the archive is reviewer-ready without manual cleanup.
4
+ //
5
+ // Archives are deterministic: entries are sorted, every entry carries a fixed mtime (the zip
6
+ // format's 1980-01-01 epoch, built from local wall-clock fields so the stored bytes don't vary
7
+ // with the machine's timezone), and the compression level is pinned. Same tree → same bytes.
8
+ import { promises as fs } from "node:fs";
9
+ import * as path from "node:path";
10
+ import { zipSync } from "fflate";
11
+ import { resolveWorkspacePath, resolveWorkspacePathReal, WorkspacePathEscapeError, } from "./workspace.js";
12
+ export class ZipError extends Error {
13
+ cause;
14
+ constructor(message, cause) {
15
+ super(message);
16
+ this.cause = cause;
17
+ this.name = "ZipError";
18
+ }
19
+ }
20
+ const DEFAULT_INCLUDES = ["flows/", "docs/", ".docsxai.json", "auth/strategy.yaml", "README.md"];
21
+ const FIXED_MTIME = new Date(1980, 0, 1);
22
+ const FIXED_LEVEL = 6;
23
+ function isExcluded(relPath, includeViewer) {
24
+ const segments = relPath.split("/");
25
+ if (segments.includes(".auth") || segments.includes("halts"))
26
+ return true;
27
+ if (!includeViewer && segments[0] === ".viewer")
28
+ return true;
29
+ const base = segments[segments.length - 1];
30
+ return base === ".DS_Store" || base.endsWith(".tmp");
31
+ }
32
+ async function collectFiles(workspace, relDir, includeViewer, out) {
33
+ const entries = await fs.readdir(resolveWorkspacePath(workspace, relDir), {
34
+ withFileTypes: true,
35
+ });
36
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
37
+ for (const entry of entries) {
38
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
39
+ if (isExcluded(rel, includeViewer))
40
+ continue;
41
+ if (entry.isDirectory()) {
42
+ await collectFiles(workspace, rel, includeViewer, out);
43
+ }
44
+ else if (entry.isFile()) {
45
+ out.push(rel);
46
+ }
47
+ else if (entry.isSymbolicLink()) {
48
+ // The archive is a hand-off artifact: follow a symlink only if its target stays inside the
49
+ // workspace — a link escaping the root must not exfiltrate whatever it points at.
50
+ let stat;
51
+ try {
52
+ await resolveWorkspacePathReal(workspace, rel);
53
+ stat = await fs.stat(resolveWorkspacePath(workspace, rel));
54
+ }
55
+ catch (e) {
56
+ if (e instanceof WorkspacePathEscapeError)
57
+ continue;
58
+ continue; // broken symlink — nothing to archive
59
+ }
60
+ if (stat.isDirectory())
61
+ await collectFiles(workspace, rel, includeViewer, out);
62
+ else if (stat.isFile())
63
+ out.push(rel);
64
+ }
65
+ }
66
+ }
67
+ export async function zipDocPack(opts) {
68
+ const { workspace, output, includeViewer = false } = opts;
69
+ try {
70
+ await fs.access(workspace);
71
+ }
72
+ catch {
73
+ throw new ZipError(`workspace doesn't exist: ${workspace}`);
74
+ }
75
+ const outAbs = path.resolve(output);
76
+ await fs.mkdir(path.dirname(outAbs), { recursive: true });
77
+ const includesAll = includeViewer ? [...DEFAULT_INCLUDES, ".viewer/"] : DEFAULT_INCLUDES;
78
+ const files = [];
79
+ let anyIncludePresent = false;
80
+ for (const inc of includesAll) {
81
+ const rel = inc.endsWith("/") ? inc.slice(0, -1) : inc;
82
+ let stat;
83
+ try {
84
+ stat = await fs.stat(resolveWorkspacePath(workspace, rel));
85
+ }
86
+ catch {
87
+ continue; // skip absent
88
+ }
89
+ anyIncludePresent = true;
90
+ if (stat.isDirectory())
91
+ await collectFiles(workspace, rel, includeViewer, files);
92
+ else
93
+ files.push(rel);
94
+ }
95
+ if (!anyIncludePresent) {
96
+ throw new ZipError(`workspace ${workspace} has nothing to zip (no flows/ or docs/ found)`);
97
+ }
98
+ files.sort();
99
+ const zippable = {};
100
+ for (const rel of files) {
101
+ const data = await fs.readFile(resolveWorkspacePath(workspace, rel));
102
+ zippable[rel] = [new Uint8Array(data), { level: FIXED_LEVEL, mtime: FIXED_MTIME }];
103
+ }
104
+ let bytes;
105
+ try {
106
+ bytes = zipSync(zippable, { level: FIXED_LEVEL, mtime: FIXED_MTIME });
107
+ }
108
+ catch (e) {
109
+ throw new ZipError(`failed to build archive: ${e.message}`, e);
110
+ }
111
+ await fs.writeFile(outAbs, bytes);
112
+ return { output: outAbs, bytes: bytes.length, entries: files };
113
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@docsxai/engine",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "provenance": true
8
+ },
9
+ "description": "LLM-agnostic engine: flow-file parser + deterministic runtime + the docsxai CLI + the target-site auth-strategy layer.",
10
+ "license": "Apache-2.0",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/kalebteccom/docsxai.git",
14
+ "directory": "packages/engine"
15
+ },
16
+ "homepage": "https://github.com/kalebteccom/docsxai#readme",
17
+ "bugs": "https://github.com/kalebteccom/docsxai/issues",
18
+ "keywords": [
19
+ "docsxai",
20
+ "documentation",
21
+ "playwright",
22
+ "claude-code",
23
+ "agent"
24
+ ],
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./cli": {
33
+ "default": "./dist/cli.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "bin": {
40
+ "docsxai": "./dist/cli.js"
41
+ },
42
+ "dependencies": {
43
+ "fflate": "^0.8.3",
44
+ "playwright-core": "^1.61.0",
45
+ "pngjs": "^7.0.0",
46
+ "yaml": "^2.5.0",
47
+ "zod": "^3.23.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20.14.0",
51
+ "@types/pngjs": "^6.0.5",
52
+ "vitest": "^2.0.0",
53
+ "@docsxai/backend": "0.2.0"
54
+ },
55
+ "author": "Kalebtec <security@kalebtec.com>",
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "scripts": {
60
+ "build": "node ../../scripts/clean-dist.mjs && tsc -b tsconfig.build.json",
61
+ "typecheck": "tsc --noEmit",
62
+ "test": "vitest run --passWithNoTests"
63
+ }
64
+ }