toolfactory 0.1.1

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 (228) hide show
  1. package/README.md +166 -0
  2. package/dist/bindings/index.d.ts +23 -0
  3. package/dist/bindings/index.js +29 -0
  4. package/dist/bindings/python.d.ts +33 -0
  5. package/dist/bindings/python.js +805 -0
  6. package/dist/bindings/python.test.d.ts +1 -0
  7. package/dist/bindings/python.test.js +316 -0
  8. package/dist/bindings/typescript.d.ts +39 -0
  9. package/dist/bindings/typescript.js +758 -0
  10. package/dist/bindings/typescript.test.d.ts +1 -0
  11. package/dist/bindings/typescript.test.js +264 -0
  12. package/dist/commands.d.ts +188 -0
  13. package/dist/commands.js +662 -0
  14. package/dist/commands.test.d.ts +1 -0
  15. package/dist/commands.test.js +215 -0
  16. package/dist/e2e.test.d.ts +1 -0
  17. package/dist/e2e.test.js +124 -0
  18. package/dist/hosts/browser.d.ts +10 -0
  19. package/dist/hosts/browser.js +94 -0
  20. package/dist/hosts/github.d.ts +66 -0
  21. package/dist/hosts/github.js +247 -0
  22. package/dist/hosts/openclaw.d.ts +4 -0
  23. package/dist/hosts/openclaw.js +94 -0
  24. package/dist/hosts/template.d.ts +15 -0
  25. package/dist/hosts/template.js +142 -0
  26. package/dist/hosts/web.d.ts +4 -0
  27. package/dist/hosts/web.js +89 -0
  28. package/dist/identity/name.d.ts +20 -0
  29. package/dist/identity/name.js +33 -0
  30. package/dist/identity/name.test.d.ts +1 -0
  31. package/dist/identity/name.test.js +37 -0
  32. package/dist/identity/read.d.ts +9 -0
  33. package/dist/identity/read.js +106 -0
  34. package/dist/identity/read.test.d.ts +1 -0
  35. package/dist/identity/read.test.js +76 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +6 -0
  38. package/dist/introspect/index.d.ts +15 -0
  39. package/dist/introspect/index.js +63 -0
  40. package/dist/model.d.ts +236 -0
  41. package/dist/model.js +214 -0
  42. package/dist/ops.d.ts +255 -0
  43. package/dist/ops.js +297 -0
  44. package/dist/project/apply.d.ts +35 -0
  45. package/dist/project/apply.js +360 -0
  46. package/dist/project/apply.test.d.ts +1 -0
  47. package/dist/project/apply.test.js +285 -0
  48. package/dist/project/gate.d.ts +129 -0
  49. package/dist/project/gate.js +597 -0
  50. package/dist/project/history.d.ts +62 -0
  51. package/dist/project/history.js +41 -0
  52. package/dist/project/load.d.ts +21 -0
  53. package/dist/project/load.js +82 -0
  54. package/dist/project/lock.d.ts +28 -0
  55. package/dist/project/lock.js +42 -0
  56. package/dist/project/plan.d.ts +5 -0
  57. package/dist/project/plan.js +96 -0
  58. package/dist/report/coverage.d.ts +22 -0
  59. package/dist/report/coverage.js +72 -0
  60. package/dist/report/coverage.test.d.ts +1 -0
  61. package/dist/report/coverage.test.js +93 -0
  62. package/dist/surfaces/agent-plugins.d.ts +10 -0
  63. package/dist/surfaces/agent-plugins.js +36 -0
  64. package/dist/surfaces/agents.d.ts +35 -0
  65. package/dist/surfaces/agents.js +609 -0
  66. package/dist/surfaces/agents.template.d.ts +8 -0
  67. package/dist/surfaces/agents.template.js +21 -0
  68. package/dist/surfaces/agents.test.d.ts +1 -0
  69. package/dist/surfaces/agents.test.js +181 -0
  70. package/dist/surfaces/browser-extension.d.ts +101 -0
  71. package/dist/surfaces/browser-extension.js +1034 -0
  72. package/dist/surfaces/browser-extension.test.d.ts +1 -0
  73. package/dist/surfaces/browser-extension.test.js +145 -0
  74. package/dist/surfaces/claude.d.ts +2 -0
  75. package/dist/surfaces/claude.js +81 -0
  76. package/dist/surfaces/codex.d.ts +13 -0
  77. package/dist/surfaces/codex.js +101 -0
  78. package/dist/surfaces/codex.test.d.ts +1 -0
  79. package/dist/surfaces/codex.test.js +67 -0
  80. package/dist/surfaces/cursor.d.ts +2 -0
  81. package/dist/surfaces/cursor.js +50 -0
  82. package/dist/surfaces/distribution.test.d.ts +1 -0
  83. package/dist/surfaces/distribution.test.js +146 -0
  84. package/dist/surfaces/dsh.d.ts +25 -0
  85. package/dist/surfaces/dsh.js +147 -0
  86. package/dist/surfaces/dsh.test.d.ts +1 -0
  87. package/dist/surfaces/dsh.test.js +100 -0
  88. package/dist/surfaces/external.d.ts +9 -0
  89. package/dist/surfaces/external.js +4 -0
  90. package/dist/surfaces/gemini.d.ts +29 -0
  91. package/dist/surfaces/gemini.js +68 -0
  92. package/dist/surfaces/gemini.test.d.ts +1 -0
  93. package/dist/surfaces/gemini.test.js +74 -0
  94. package/dist/surfaces/hermes-native.d.ts +53 -0
  95. package/dist/surfaces/hermes-native.js +507 -0
  96. package/dist/surfaces/hermes-native.test.d.ts +1 -0
  97. package/dist/surfaces/hermes-native.test.js +156 -0
  98. package/dist/surfaces/kernel.d.ts +5 -0
  99. package/dist/surfaces/kernel.js +63 -0
  100. package/dist/surfaces/mcp-registry.d.ts +19 -0
  101. package/dist/surfaces/mcp-registry.js +160 -0
  102. package/dist/surfaces/mcp-registry.test.d.ts +1 -0
  103. package/dist/surfaces/mcp-registry.test.js +79 -0
  104. package/dist/surfaces/mcpb.d.ts +11 -0
  105. package/dist/surfaces/mcpb.js +118 -0
  106. package/dist/surfaces/mcpb.test.d.ts +1 -0
  107. package/dist/surfaces/mcpb.test.js +104 -0
  108. package/dist/surfaces/npm.d.ts +6 -0
  109. package/dist/surfaces/npm.js +72 -0
  110. package/dist/surfaces/openclaw-native.d.ts +93 -0
  111. package/dist/surfaces/openclaw-native.js +956 -0
  112. package/dist/surfaces/openclaw-native.test.d.ts +1 -0
  113. package/dist/surfaces/openclaw-native.test.js +246 -0
  114. package/dist/surfaces/pypi.d.ts +5 -0
  115. package/dist/surfaces/pypi.js +82 -0
  116. package/dist/surfaces/readme.d.ts +7 -0
  117. package/dist/surfaces/readme.js +181 -0
  118. package/dist/surfaces/registry.d.ts +6 -0
  119. package/dist/surfaces/registry.js +73 -0
  120. package/dist/surfaces/shared.d.ts +49 -0
  121. package/dist/surfaces/shared.js +112 -0
  122. package/dist/surfaces/skill.d.ts +7 -0
  123. package/dist/surfaces/skill.js +118 -0
  124. package/dist/surfaces/skill.test.d.ts +1 -0
  125. package/dist/surfaces/skill.test.js +87 -0
  126. package/dist/surfaces/web.d.ts +97 -0
  127. package/dist/surfaces/web.js +1285 -0
  128. package/dist/surfaces/web.test.d.ts +1 -0
  129. package/dist/surfaces/web.test.js +255 -0
  130. package/dist/surfaces/workflows.d.ts +2 -0
  131. package/dist/surfaces/workflows.js +809 -0
  132. package/dist/surfaces/workflows.test.d.ts +1 -0
  133. package/dist/surfaces/workflows.test.js +563 -0
  134. package/dist/toolfactory/cli.d.ts +2 -0
  135. package/dist/toolfactory/cli.js +65 -0
  136. package/dist/toolfactory/config.d.ts +3 -0
  137. package/dist/toolfactory/config.js +27 -0
  138. package/dist/toolfactory/mcp.d.ts +37 -0
  139. package/dist/toolfactory/mcp.js +269 -0
  140. package/dist/toolfactory/types.d.ts +31 -0
  141. package/dist/toolfactory/types.js +10 -0
  142. package/dist/toolfactory/web.d.ts +4 -0
  143. package/dist/toolfactory/web.js +40 -0
  144. package/dist/validate/agent-plugins.d.ts +5 -0
  145. package/dist/validate/agent-plugins.js +46 -0
  146. package/package.json +68 -0
  147. package/schemas/agent-plugins/mcp.schema.json +120 -0
  148. package/schemas/agent-plugins/plugin.schema.json +65 -0
  149. package/src/bindings/index.ts +47 -0
  150. package/src/bindings/python.test.ts +374 -0
  151. package/src/bindings/python.ts +862 -0
  152. package/src/bindings/typescript.test.ts +329 -0
  153. package/src/bindings/typescript.ts +818 -0
  154. package/src/commands.test.ts +253 -0
  155. package/src/commands.ts +886 -0
  156. package/src/e2e.test.ts +169 -0
  157. package/src/hosts/browser.ts +106 -0
  158. package/src/hosts/github.ts +321 -0
  159. package/src/hosts/openclaw.ts +110 -0
  160. package/src/hosts/template.ts +149 -0
  161. package/src/hosts/web.ts +111 -0
  162. package/src/identity/name.test.ts +43 -0
  163. package/src/identity/name.ts +39 -0
  164. package/src/identity/read.test.ts +92 -0
  165. package/src/identity/read.ts +119 -0
  166. package/src/index.ts +11 -0
  167. package/src/introspect/index.ts +72 -0
  168. package/src/model.ts +386 -0
  169. package/src/ops.ts +333 -0
  170. package/src/project/apply.test.ts +348 -0
  171. package/src/project/apply.ts +417 -0
  172. package/src/project/gate.ts +717 -0
  173. package/src/project/history.ts +46 -0
  174. package/src/project/load.ts +104 -0
  175. package/src/project/lock.ts +54 -0
  176. package/src/project/plan.ts +101 -0
  177. package/src/report/coverage.test.ts +108 -0
  178. package/src/report/coverage.ts +113 -0
  179. package/src/surfaces/agent-plugins.ts +47 -0
  180. package/src/surfaces/agents.template.ts +32 -0
  181. package/src/surfaces/agents.test.ts +206 -0
  182. package/src/surfaces/agents.ts +729 -0
  183. package/src/surfaces/browser-extension.test.ts +175 -0
  184. package/src/surfaces/browser-extension.ts +1095 -0
  185. package/src/surfaces/claude.ts +99 -0
  186. package/src/surfaces/codex.test.ts +71 -0
  187. package/src/surfaces/codex.ts +105 -0
  188. package/src/surfaces/cursor.ts +62 -0
  189. package/src/surfaces/distribution.test.ts +166 -0
  190. package/src/surfaces/dsh.test.ts +110 -0
  191. package/src/surfaces/dsh.ts +159 -0
  192. package/src/surfaces/external.ts +13 -0
  193. package/src/surfaces/gemini.test.ts +80 -0
  194. package/src/surfaces/gemini.ts +84 -0
  195. package/src/surfaces/hermes-native.test.ts +179 -0
  196. package/src/surfaces/hermes-native.ts +584 -0
  197. package/src/surfaces/kernel.ts +68 -0
  198. package/src/surfaces/mcp-registry.test.ts +99 -0
  199. package/src/surfaces/mcp-registry.ts +190 -0
  200. package/src/surfaces/mcpb.test.ts +112 -0
  201. package/src/surfaces/mcpb.ts +130 -0
  202. package/src/surfaces/npm.ts +78 -0
  203. package/src/surfaces/openclaw-native.test.ts +283 -0
  204. package/src/surfaces/openclaw-native.ts +1070 -0
  205. package/src/surfaces/pypi.ts +84 -0
  206. package/src/surfaces/readme.ts +218 -0
  207. package/src/surfaces/registry.ts +79 -0
  208. package/src/surfaces/shared.ts +140 -0
  209. package/src/surfaces/skill.test.ts +103 -0
  210. package/src/surfaces/skill.ts +136 -0
  211. package/src/surfaces/web.test.ts +303 -0
  212. package/src/surfaces/web.ts +1310 -0
  213. package/src/surfaces/workflows.test.ts +677 -0
  214. package/src/surfaces/workflows.ts +936 -0
  215. package/src/toolfactory/cli.ts +74 -0
  216. package/src/toolfactory/config.ts +33 -0
  217. package/src/toolfactory/mcp.ts +311 -0
  218. package/src/toolfactory/types.ts +52 -0
  219. package/src/toolfactory/web.ts +45 -0
  220. package/src/validate/agent-plugins.ts +53 -0
  221. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  222. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  223. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  224. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  225. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  226. package/web/dist/assets/index-CCXhQKK3.css +2 -0
  227. package/web/dist/assets/index-D1PwZ3qS.js +10 -0
  228. package/web/dist/index.html +14 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,255 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { describe, expect, it } from "vitest";
6
+ import { kernel, scaffold } from "../bindings/typescript.js";
7
+ import { run } from "../commands.js";
8
+ import { DRIFT_ENTRY } from "../hosts/web.js";
9
+ import { apply, deepMerge, replaceRegions } from "../project/apply.js";
10
+ import { json } from "./shared.js";
11
+ import { APP_BEGIN, APP_END, surface, WEB_DIR, WEB_SCAFFOLD } from "./web.js";
12
+ const echo = {
13
+ name: "echo",
14
+ description: "Return the text you pass in.",
15
+ inputSchema: {
16
+ $schema: "https://json-schema.org/draft/2020-12/schema",
17
+ type: "object",
18
+ properties: { text: { type: "string", description: "Text to echo back" } },
19
+ required: ["text"],
20
+ },
21
+ annotations: { readOnlyHint: true },
22
+ requires: [],
23
+ };
24
+ const shoot = {
25
+ name: "shoot",
26
+ description: "Take a screenshot of the current tab.",
27
+ inputSchema: { type: "object" },
28
+ requires: ["browser"],
29
+ };
30
+ function project(overrides = {}) {
31
+ return {
32
+ root: "/repo",
33
+ tool: {
34
+ schemaVersion: 1,
35
+ identity: "package.json",
36
+ binding: "typescript",
37
+ surfaces: ["web", "mcp", "cli"],
38
+ bundle: { runtime: "package" },
39
+ tests: { examples: {} },
40
+ },
41
+ identity: { name: "hello", version: "0.1.0", description: "Say hello" },
42
+ identityExtra: {},
43
+ operations: [echo, shoot],
44
+ toolfactoryVersion: "0.1.0",
45
+ ...overrides,
46
+ };
47
+ }
48
+ /** Every planned file as text, region files rendered fresh (as `apply()` writes them the first time). */
49
+ function planned(from = project()) {
50
+ return new Map(surface.plan(from).map((file) => {
51
+ if (file.kind === "file")
52
+ return [file.path, file.content];
53
+ if (file.kind === "region")
54
+ return [file.path, replaceRegions(file.template, file) ?? file.template];
55
+ return [file.path, json(deepMerge({}, file.patch))];
56
+ }));
57
+ }
58
+ function opsJson(from = project()) {
59
+ return JSON.parse(planned(from).get(`${WEB_DIR}/src/ops.json`) ?? "");
60
+ }
61
+ describe("web", () => {
62
+ it("plans the vite + shadcn project and leaves src/components/ui to the shadcn CLI", () => {
63
+ expect([...planned().keys()]).toEqual([
64
+ "web/.oxlintrc.json",
65
+ "web/components.json",
66
+ "web/index.html",
67
+ "web/package.json",
68
+ "web/smoke.mjs",
69
+ "web/src/App.tsx",
70
+ "web/src/index.css",
71
+ "web/src/lib/utils.ts",
72
+ "web/src/main.tsx",
73
+ "web/src/ops.json",
74
+ "web/src/schema-form.tsx",
75
+ "web/tsconfig.app.json",
76
+ "web/tsconfig.json",
77
+ "web/tsconfig.node.json",
78
+ "web/vite.config.ts",
79
+ ]);
80
+ });
81
+ it("App.tsx is a region file: the operations page is generated, the pages array and shell are the author's and survive a rebuild", () => {
82
+ const target = project();
83
+ const file = surface
84
+ .plan(target)
85
+ .find((candidate) => candidate.path === `${WEB_DIR}/src/App.tsx`);
86
+ if (file?.kind !== "region")
87
+ throw new Error("web/src/App.tsx is no longer a region file");
88
+ expect(file.regions).toHaveLength(1);
89
+ expect(file.regions[0].begin).toBe(APP_BEGIN);
90
+ expect(file.regions[0].end).toBe(APP_END);
91
+ expect(file.regions[0].content).toContain("export function OperationsPage()");
92
+ expect(file.template).toContain("export const pages: { id: string; title: string; element: ReactNode }[]");
93
+ expect(file.template).toContain('{ id: "operations", title: "Operations", element: <OperationsPage /> }');
94
+ expect(file.template).toContain("export default function App()");
95
+ // Round-trip through the real writer: scaffold, let the author add a page below the
96
+ // marker (as vutoolkit adds its d3 degree-planner graph), then run a second `toolfactory
97
+ // build` (a changed operation set, same as a real `introspect` picking up new tools) and
98
+ // check the added page is still there.
99
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-web-app-"));
100
+ apply(root, surface.plan(target), "0.1.0");
101
+ const appPath = join(root, WEB_DIR, "src/App.tsx");
102
+ const authored = readFileSync(appPath, "utf8").replace('{ id: "operations", title: "Operations", element: <OperationsPage /> },', '{ id: "operations", title: "Operations", element: <OperationsPage /> },\n { id: "planner", title: "Degree planner", element: <div>planner</div> },');
103
+ writeFileSync(appPath, authored);
104
+ const rebuilt = project({ operations: [echo] });
105
+ apply(root, surface.plan(rebuilt), "0.1.0");
106
+ const after = readFileSync(appPath, "utf8");
107
+ expect(after).toContain('{ id: "planner", title: "Degree planner", element: <div>planner</div> },');
108
+ expect(after).toContain("export function OperationsPage()");
109
+ });
110
+ it("web/package.json is a merge file, so an author-added dependency (e.g. `npm --prefix web install d3` for a page) survives a rebuild", () => {
111
+ const target = project();
112
+ const file = surface
113
+ .plan(target)
114
+ .find((candidate) => candidate.path === `${WEB_DIR}/package.json`);
115
+ if (file?.kind !== "merge")
116
+ throw new Error("web/package.json is no longer a merge file");
117
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-web-pkg-"));
118
+ apply(root, surface.plan(target), "0.1.0");
119
+ const pkgPath = join(root, WEB_DIR, "package.json");
120
+ const authored = JSON.parse(readFileSync(pkgPath, "utf8"));
121
+ authored.dependencies.d3 = "^7.9.0";
122
+ writeFileSync(pkgPath, json(authored));
123
+ apply(root, surface.plan(project({ operations: [echo] })), "0.1.0");
124
+ const after = JSON.parse(readFileSync(pkgPath, "utf8"));
125
+ expect(after.dependencies.d3).toBe("^7.9.0");
126
+ expect(after.dependencies.react).toBe(WEB_SCAFFOLD.packageJson.dependencies.react);
127
+ });
128
+ it("snapshots the operations this surface carries and says why the others are missing", () => {
129
+ const data = opsJson();
130
+ expect(data.tool).toEqual({ name: "hello", version: "0.1.0", description: "Say hello" });
131
+ expect(data.mcpProtocolVersion).toBe("2026-07-28");
132
+ // Relative to the document: one build serves under `/`, under a plugin prefix, and on Pages.
133
+ expect(data.defaultEndpoint).toBe("mcp");
134
+ const operations = data.operations;
135
+ expect(operations.map((operation) => operation.name)).toEqual(["echo"]);
136
+ expect(operations[0]?.inputSchema).toEqual(echo.inputSchema);
137
+ // `smoke.mjs` runs the first read-only operation for real, so the hints have to travel.
138
+ expect(operations[0]?.annotations).toEqual({ readOnlyHint: true });
139
+ expect(data.excluded).toEqual([{ name: "shoot", reason: "excluded:mcp-no-host-capabilities" }]);
140
+ expect(surface.verdict?.(shoot, project())).toEqual({
141
+ kind: "excluded",
142
+ reason: "excluded:mcp-no-host-capabilities",
143
+ });
144
+ });
145
+ it("src/ops.json is an output file: a lossy duplicate rebuilt by `npm run build`, not tracked (D8)", () => {
146
+ const file = surface
147
+ .plan(project())
148
+ .find((candidate) => candidate.path === `${WEB_DIR}/src/ops.json`);
149
+ if (file?.kind !== "file")
150
+ throw new Error("web/src/ops.json is no longer a plain file");
151
+ expect(file.output).toBe(true);
152
+ });
153
+ it("the page and the smoke both talk to the kernel that served them: relative endpoint, bearer from the fragment, one listener", () => {
154
+ const files = planned();
155
+ const app = files.get(`${WEB_DIR}/src/App.tsx`) ?? "";
156
+ expect(app).toContain("new URL(DATA.defaultEndpoint, document.baseURI).href");
157
+ expect(app).toContain("window.location.hash");
158
+ expect(app).toContain("Authorization: `Bearer ${TOKEN}`");
159
+ // The Secrets panel is the developer's `.env`, and it is the kernel's `/env` that answers.
160
+ expect(app).toContain('new URL("env", document.baseURI).href');
161
+ expect(app).toContain("toolfactory secrets check");
162
+ expect(app).toContain('type="password"');
163
+ // The smoke drives the real kernel over --http instead of a static server of its own.
164
+ const smoke = files.get(`${WEB_DIR}/smoke.mjs`) ?? "";
165
+ expect(smoke).toContain('const KERNEL = ["node","--import","tsx","src/toolfactory/mcp.ts"]');
166
+ expect(smoke).toContain('"--http", "0"');
167
+ expect(smoke).toContain("HELLO_MCP_TOKEN");
168
+ expect(smoke).toContain("candidate.annotations?.readOnlyHint");
169
+ expect(smoke).not.toContain("createServer");
170
+ });
171
+ it("builds with a relative base, so one build is served the same at /, under a plugin prefix and on Pages", () => {
172
+ const content = planned().get(`${WEB_DIR}/vite.config.ts`) ?? "";
173
+ expect(content).toContain('base: process.env.PAGES_BASE ?? "./"');
174
+ });
175
+ it("previews the CLI only when the cli surface is also selected", () => {
176
+ expect(opsJson().cliAvailable).toBe(true);
177
+ const without = project({ tool: { ...project().tool, surfaces: ["web", "mcp"] } });
178
+ expect(opsJson(without).cliAvailable).toBe(false);
179
+ });
180
+ it("emits the pinned scaffold verbatim, so the drift check speaks for plan()", () => {
181
+ const files = planned();
182
+ expect(files.get("web/src/index.css")).toBe(WEB_SCAFFOLD.indexCss);
183
+ expect(files.get("web/src/lib/utils.ts")).toBe(WEB_SCAFFOLD.utils);
184
+ for (const [path, content] of Object.entries(WEB_SCAFFOLD.vite)) {
185
+ // The two tsconfigs gain the `@/*` alias; the rest travel unchanged.
186
+ const emitted = files.get(`web/${path}`) ?? "";
187
+ if (path === "tsconfig.json" || path === "tsconfig.app.json") {
188
+ expect(emitted).toContain('"@/*": ["./src/*"]');
189
+ }
190
+ else if (path !== "vite.config.ts") {
191
+ expect(emitted).toBe(content);
192
+ }
193
+ }
194
+ expect(JSON.parse(files.get("web/components.json") ?? "")).toEqual(WEB_SCAFFOLD.componentsJson);
195
+ const packageJson = JSON.parse(files.get("web/package.json") ?? "");
196
+ expect(packageJson.dependencies).toEqual(WEB_SCAFFOLD.packageJson.dependencies);
197
+ expect(packageJson.devDependencies).toMatchObject(WEB_SCAFFOLD.packageJson.devDependencies);
198
+ expect(packageJson.devDependencies.playwright).toBe(WEB_SCAFFOLD.playwright);
199
+ // Every component the page imports is one `validate()` asks the shadcn CLI for.
200
+ const sources = `${files.get("web/src/App.tsx")}${files.get("web/src/schema-form.tsx")}`;
201
+ const imported = [...sources.matchAll(/@\/components\/ui\/([a-z-]+)/g)].map((match) => match[1]);
202
+ expect(imported.length).toBeGreaterThan(0);
203
+ expect(WEB_SCAFFOLD.components).toEqual(expect.arrayContaining(imported));
204
+ });
205
+ });
206
+ /**
207
+ * The whole validator chain, run the way `toolfactory validate --surface web` runs it. Needs
208
+ * npm (every step installs from the registry) and a Playwright browser download for the smoke,
209
+ * so it self-skips where neither the author nor CI has one.
210
+ */
211
+ function browserAvailable() {
212
+ const browsers = process.env.PLAYWRIGHT_BROWSERS_PATH ?? join(homedir(), ".cache/ms-playwright");
213
+ return existsSync(browsers) && spawnSync("npm", ["--version"]).status === 0;
214
+ }
215
+ describe.skipIf(!browserAvailable())("validated the way an author runs it", () => {
216
+ it("installs, adds the components, proves the scaffold, builds and smokes the page", () => {
217
+ const root = mkdtempSync(join(tmpdir(), "toolfactory-web-"));
218
+ const fixture = project({ root });
219
+ for (const [path, content] of planned(fixture)) {
220
+ mkdirSync(dirname(join(root, path)), { recursive: true });
221
+ writeFileSync(join(root, path), content);
222
+ }
223
+ // What `loadProject` needs, so the drift script can re-plan from the same project.
224
+ mkdirSync(join(root, "dev.toolfactory"), { recursive: true });
225
+ writeFileSync(join(root, "dev.toolfactory/tool.json"), json(fixture.tool));
226
+ writeFileSync(join(root, "dev.toolfactory/ops.json"), json({
227
+ tools: [echo, shoot].map(({ requires, ...tool }) => ({
228
+ ...tool,
229
+ _meta: { "dev.toolfactory": { requires } },
230
+ })),
231
+ }));
232
+ // The smoke drives the real kernel, so the fixture is a real project: the binding's kernel
233
+ // and scaffold, and this checkout's `node_modules` (the SDK, zod and tsx the kernel imports).
234
+ for (const file of [...kernel(fixture), ...scaffold(fixture)]) {
235
+ if (file.kind !== "file")
236
+ continue;
237
+ mkdirSync(dirname(join(root, file.path)), { recursive: true });
238
+ writeFileSync(join(root, file.path), file.content);
239
+ }
240
+ symlinkSync(join(process.cwd(), "node_modules"), join(root, "node_modules"));
241
+ for (const command of surface.validate?.(fixture) ?? []) {
242
+ // Vitest runs this file from source, so the drift entry is still TypeScript: give node
243
+ // the loader from this checkout, where `tsx` is installed.
244
+ const outcome = run(command.args.includes(DRIFT_ENTRY)
245
+ ? { ...command, args: ["--import", "tsx", DRIFT_ENTRY, root], cwd: process.cwd() }
246
+ : command);
247
+ expect(outcome.ok, `${outcome.label}: ${outcome.output}`).toBe(true);
248
+ if (outcome.label.includes("smoke")) {
249
+ expect(outcome.output).toContain("PASS cli preview");
250
+ // The whole point of driving the kernel: the page's own call came back from it.
251
+ expect(outcome.output).toContain('PASS echo round-tripped: {\n "text": "smoke"\n}');
252
+ }
253
+ }
254
+ }, 900_000);
255
+ });
@@ -0,0 +1,2 @@
1
+ import { type Surface } from "../model.js";
2
+ export declare const surface: Surface;