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,215 @@
1
+ /**
2
+ * The command layer's own behaviour: the step runner behind `gate`/`package`, the `gh`
3
+ * invocations `init --repo` makes (dry-run only — `gh` is not installed here and
4
+ * api.github.com is unreachable, so the shape is proved, never the call), and what `init`
5
+ * leaves behind in a real directory.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterEach, describe, expect, it } from "vitest";
12
+ import * as commands from "./commands.js";
13
+ import { createRepo } from "./hosts/github.js";
14
+ import { assertNoSensitiveArgument, defaultSurfaces, ToolConfigSchema, } from "./model.js";
15
+ import { RELOAD } from "./surfaces/agents.js";
16
+ function project(surfaces) {
17
+ return {
18
+ root: "/repo",
19
+ tool: {
20
+ schemaVersion: 1,
21
+ identity: "plugin.json",
22
+ binding: "typescript",
23
+ surfaces,
24
+ bundle: { runtime: "package" },
25
+ tests: { examples: {} },
26
+ },
27
+ identity: { name: "probe", version: "0.1.0", description: "A probe tool" },
28
+ identityExtra: {},
29
+ operations: [],
30
+ toolfactoryVersion: "0.1.0",
31
+ };
32
+ }
33
+ const environment = { ...process.env };
34
+ afterEach(() => {
35
+ process.env = { ...environment };
36
+ });
37
+ describe("runSteps", () => {
38
+ it("runs in order, skips CI-only provisioning, and stops at the first failure", () => {
39
+ const { steps } = commands.runSteps(tmpdir(), [
40
+ { name: "first", run: "true" },
41
+ { name: "provisioning", run: "exit 1", when: "ci" },
42
+ { name: "boom", run: "exit 3" },
43
+ { name: "never", run: "true" },
44
+ ]);
45
+ expect(steps.map((step) => [step.name, step.ok])).toEqual([
46
+ ["first", true],
47
+ ["boom", false],
48
+ ]);
49
+ expect(steps.every((step) => step.durationMs >= 0)).toBe(true);
50
+ });
51
+ });
52
+ describe("createRepo", () => {
53
+ it("dry-runs the exact gh invocations: private, from the checkout, one topic per surface", () => {
54
+ expect(createRepo(project(["skill", "mcp", "openclaw-native", "cli"]), {
55
+ slug: "GoatInAHat/probe",
56
+ dryRun: true,
57
+ })).toEqual({
58
+ repository: "GoatInAHat/probe",
59
+ visibility: "private",
60
+ topics: ["agent-skill", "mcp-server", "openclaw-plugin", "toolfactory"],
61
+ commands: [
62
+ 'gh repo create GoatInAHat/probe --private --description "A probe tool" --source . --remote origin --push',
63
+ "gh repo edit GoatInAHat/probe --add-topic agent-skill,mcp-server,openclaw-plugin,toolfactory",
64
+ ],
65
+ dryRun: true,
66
+ });
67
+ });
68
+ it("makes the repository public only when asked", () => {
69
+ const { commands: lines } = createRepo(project(["mcp"]), {
70
+ slug: "GoatInAHat/probe",
71
+ public: true,
72
+ dryRun: true,
73
+ });
74
+ expect(lines[0]).toContain("--public");
75
+ expect(lines[0]).not.toContain("--private");
76
+ });
77
+ });
78
+ describe("init", () => {
79
+ it("initialises a git repository, commits, and reports the reload line of this harness", () => {
80
+ const dir = mkdtempSync(join(tmpdir(), "toolfactory-init-"));
81
+ process.env.CLAUDECODE = "1";
82
+ // No git identity, as on a CI runner: the first commit must still land.
83
+ process.env.GIT_CONFIG_GLOBAL = "/dev/null";
84
+ process.env.GIT_CONFIG_SYSTEM = "/dev/null";
85
+ // `setup: false`: `.agents/setup` installs dependencies, which belongs to the end-to-end
86
+ // proof in a real project, not to a unit test.
87
+ const result = commands.init({
88
+ root: dir,
89
+ name: "probe",
90
+ binding: "typescript",
91
+ surfaces: ["cli"],
92
+ repo: "GoatInAHat/probe",
93
+ dryRun: true,
94
+ setup: false,
95
+ });
96
+ expect(existsSync(join(dir, ".git"))).toBe(true);
97
+ expect(execFileSync("git", ["log", "--format=%s"], { cwd: dir, encoding: "utf8" }).trim()).toBe("toolfactory init");
98
+ expect(result.repository?.commands[0]).toBe("gh repo create GoatInAHat/probe --private --source . --remote origin --push");
99
+ // Nothing ran: no remote, and no live-tests bootstrap without a declared credential.
100
+ expect(execFileSync("git", ["remote"], { cwd: dir, encoding: "utf8" }).trim()).toBe("");
101
+ expect(result.repository?.secrets).toEqual([]);
102
+ expect(result.nextSteps[1]).toBe(RELOAD.find((row) => row.harness === "Claude Code")?.line);
103
+ expect(result.nextSteps[2]).toContain("src/ops.ts");
104
+ // `keywords` defaults to `[name]` — Kiro Powers and Agent Plugins key activation off it —
105
+ // and lands in the scaffolded identity file (`package.json`, here) like the rest of identity.
106
+ expect(JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).keywords).toEqual(["probe"]);
107
+ // Asked for nothing, init selects the skills-first minimum; host plugins are opt-in.
108
+ expect(defaultSurfaces("python")).toEqual(["skill", "agent-plugins", "mcp", "cli", "pypi"]);
109
+ });
110
+ it("creates an instruction-only Codex plugin without a generated runtime", async () => {
111
+ const dir = mkdtempSync(join(tmpdir(), "toolfactory-instructions-"));
112
+ const result = commands.init({
113
+ root: dir,
114
+ name: "paper-viz",
115
+ binding: "typescript",
116
+ runtime: "none",
117
+ surfaces: ["skill", "codex"],
118
+ git: false,
119
+ setup: false,
120
+ });
121
+ expect(existsSync(join(dir, "package.json"))).toBe(false);
122
+ expect(existsSync(join(dir, "src/toolfactory/mcp.ts"))).toBe(false);
123
+ expect(existsSync(join(dir, "mcp.json"))).toBe(false);
124
+ expect(JSON.parse(readFileSync(join(dir, ".codex-plugin/plugin.json"), "utf8"))).not.toHaveProperty("mcpServers");
125
+ expect(readFileSync(join(dir, "skills/paper-viz/SKILL.md"), "utf8")).not.toContain("No operations yet");
126
+ expect(result.nextSteps[2]).toContain("SKILL.md");
127
+ await expect(commands.check(dir)).resolves.toBeDefined();
128
+ });
129
+ it("refuses runtime none with a surface that needs an executable payload", () => {
130
+ expect(() => ToolConfigSchema.parse({
131
+ schemaVersion: 1,
132
+ identity: "plugin.json",
133
+ binding: "typescript",
134
+ runtime: "none",
135
+ surfaces: ["skill", "mcp"],
136
+ })).toThrow(/skill and codex surfaces/);
137
+ });
138
+ });
139
+ describe("secrets", () => {
140
+ it("inventories the tool's sensitive keys and the registries' tokens — presence only, never a value", () => {
141
+ const dir = mkdtempSync(join(tmpdir(), "toolfactory-secrets-"));
142
+ writeFileSync(join(dir, ".env"), "API_KEY=canary-3f9a\n");
143
+ const target = project(["cli", "npm"]);
144
+ target.root = dir;
145
+ target.tool.config = {
146
+ type: "object",
147
+ properties: {
148
+ api_key: {
149
+ type: "string",
150
+ "x-toolfactory": { sensitive: true, url: "https://example.com/keys" },
151
+ },
152
+ },
153
+ required: ["api_key"],
154
+ };
155
+ const report = commands.secretsReport(target);
156
+ expect(report.secrets.map((secret) => secret.name)).toEqual(["API_KEY", "NPM_TOKEN"]);
157
+ expect(report.secrets[0]).toMatchObject({
158
+ kind: "config",
159
+ required: true,
160
+ local: true,
161
+ url: "https://example.com/keys",
162
+ });
163
+ expect(report.secrets[1]).toMatchObject({
164
+ kind: "release",
165
+ required: false,
166
+ local: false,
167
+ needs: ["npm"],
168
+ });
169
+ expect(JSON.stringify(report)).not.toContain("canary-3f9a");
170
+ });
171
+ it("refuses an operation input named like a sensitive config key", () => {
172
+ const login = {
173
+ name: "login",
174
+ inputSchema: { type: "object", properties: { password: { type: "string" } } },
175
+ requires: [],
176
+ };
177
+ expect(() => assertNoSensitiveArgument({ properties: { password: { type: "string", "x-toolfactory": { sensitive: true } } } }, [login])).toThrow(/"login" declares an input named "password"/);
178
+ });
179
+ });
180
+ describe("unpublish", () => {
181
+ it("diffs tool.json against the previous tag and retracts what a dropped surface published", () => {
182
+ const dir = mkdtempSync(join(tmpdir(), "toolfactory-unpublish-"));
183
+ process.env.GIT_CONFIG_GLOBAL = "/dev/null";
184
+ process.env.GIT_CONFIG_SYSTEM = "/dev/null";
185
+ commands.init({
186
+ root: dir,
187
+ name: "probe",
188
+ binding: "typescript",
189
+ surfaces: ["cli", "npm"],
190
+ dryRun: true,
191
+ setup: false,
192
+ });
193
+ const git = (...args) => execFileSync("git", args, {
194
+ cwd: dir,
195
+ encoding: "utf8",
196
+ env: {
197
+ ...process.env,
198
+ GIT_AUTHOR_NAME: "t",
199
+ GIT_AUTHOR_EMAIL: "t@example.com",
200
+ GIT_COMMITTER_NAME: "t",
201
+ GIT_COMMITTER_EMAIL: "t@example.com",
202
+ },
203
+ });
204
+ git("tag", "v0.1.0");
205
+ const toolPath = join(dir, "dev.toolfactory", "tool.json");
206
+ const tool = JSON.parse(readFileSync(toolPath, "utf8"));
207
+ tool.surfaces = tool.surfaces.filter((surface) => surface !== "npm");
208
+ writeFileSync(toolPath, JSON.stringify(tool, null, 2));
209
+ git("commit", "-qam", "drop npm");
210
+ git("tag", "v0.2.0");
211
+ const result = commands.unpublish(dir, { dryRun: true });
212
+ expect(result.dropped).toEqual(["npm"]);
213
+ expect(result.steps.some((step) => step.run.includes("npm deprecate probe@'*'"))).toBe(true);
214
+ });
215
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Real end-to-end: init a fresh tool, introspect the real kernel over stdio, build,
3
+ * check for drift, and run the generated CLI as a user would. Skips when this repo's
4
+ * own node_modules isn't around to symlink in (no dependencies to run against).
5
+ */
6
+ import { execFileSync } from "node:child_process";
7
+ import { existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, symlinkSync, writeFileSync, } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join, relative } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { describe, expect, it } from "vitest";
12
+ import * as commands from "./commands.js";
13
+ const repoRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..");
14
+ const repoNodeModules = join(repoRoot, "node_modules");
15
+ describe.skipIf(!existsSync(repoNodeModules))("end-to-end: init -> introspect -> build -> check -> run", () => {
16
+ it("scaffolds a real tool, snapshots its kernel, builds clean, and answers a real CLI call", {
17
+ timeout: 120_000,
18
+ }, async () => {
19
+ const dir = mkdtempSync(join(tmpdir(), "toolfactory-e2e-"));
20
+ symlinkSync(repoNodeModules, join(dir, "node_modules"), "dir");
21
+ const surfaces = ["skill", "agent-plugins", "claude", "mcp", "cli", "npm"];
22
+ const init = commands.init({
23
+ root: dir,
24
+ name: "hello",
25
+ binding: "typescript",
26
+ surfaces,
27
+ setup: false,
28
+ });
29
+ expect(init.written).toContain("dev.toolfactory/tool.json");
30
+ expect(init.written).toContain("src/ops.ts");
31
+ const snapshot = await commands.introspect(dir);
32
+ expect(snapshot.ops.tools.map((t) => t.name)).toEqual(["echo"]);
33
+ const echo = snapshot.ops.tools[0];
34
+ expect(echo.inputSchema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
35
+ expect(echo.outputSchema?.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
36
+ commands.build(dir);
37
+ await expect(commands.check(dir)).resolves.toBeDefined();
38
+ const opsOnDisk = JSON.parse(readFileSync(join(dir, "dev.toolfactory/ops.json"), "utf8"));
39
+ expect(opsOnDisk.tools.map((t) => t.name)).toEqual(["echo"]);
40
+ const output = execFileSync(process.execPath, ["--import", "tsx", "src/toolfactory/cli.ts", "echo", "--text", "hi"], { cwd: dir, encoding: "utf8" });
41
+ expect(JSON.parse(output)).toEqual({ text: "hi" });
42
+ });
43
+ });
44
+ /** Every file in the tree except the test's own scaffolding, as path → content (a link's target). */
45
+ function tree(root, dir = root) {
46
+ const files = {};
47
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
48
+ if (entry.name === "node_modules" || entry.name === ".git")
49
+ continue;
50
+ const path = join(dir, entry.name);
51
+ if (entry.isDirectory())
52
+ Object.assign(files, tree(root, path));
53
+ else if (entry.isSymbolicLink())
54
+ files[relative(root, path)] = readlinkSync(path);
55
+ else
56
+ files[relative(root, path)] = readFileSync(path, "utf8");
57
+ }
58
+ return files;
59
+ }
60
+ function scratch() {
61
+ const dir = mkdtempSync(join(tmpdir(), "toolfactory-confluence-"));
62
+ symlinkSync(repoNodeModules, join(dir, "node_modules"), "dir");
63
+ return dir;
64
+ }
65
+ /** The one thing a projector may never write: a paragraph, in the body a region file leaves alone. */
66
+ const PROSE_ANCHOR = "Explain here when an agent should reach for this tool";
67
+ const PROSE = "Reach for it when a demo needs a real tool, not a mock.";
68
+ function authorProse(root) {
69
+ const path = join(root, "skills/hello/SKILL.md");
70
+ const text = readFileSync(path, "utf8");
71
+ writeFileSync(path, text.replace(PROSE_ANCHOR, `${PROSE}\n\n${PROSE_ANCHOR}`));
72
+ }
73
+ function setSurfaces(root, surfaces) {
74
+ const path = join(root, "dev.toolfactory/tool.json");
75
+ writeFileSync(path, `${JSON.stringify({ ...JSON.parse(readFileSync(path, "utf8")), surfaces }, null, 2)}\n`);
76
+ }
77
+ /**
78
+ * Confluence: a tool that got where it is by editing, toggling and adopting is byte-identical to
79
+ * one generated from its `tool.json` in a single step. Every generated file therefore carries its
80
+ * own inverse — an orphan its deletion, a merge file the keys the lock records — and the drift
81
+ * gate is complete: a clean `check` means the tree is exactly what a fresh build would write.
82
+ */
83
+ describe.skipIf(!existsSync(repoNodeModules))("confluence: a walked tree equals a fresh one", () => {
84
+ it("converges on the same tree as init + build of the final tool.json", {
85
+ timeout: 180_000,
86
+ }, async () => {
87
+ const identity = {
88
+ name: "hello",
89
+ binding: "typescript",
90
+ repository: "https://github.com/toolfactory/hello",
91
+ };
92
+ const start = ["skill", "cli", "npm", "mcp-registry"];
93
+ const end = ["skill", "cli", "npm"];
94
+ const walked = scratch();
95
+ commands.init({ ...identity, root: walked, surfaces: start, setup: false });
96
+ await commands.introspect(walked);
97
+ setSurfaces(walked, end);
98
+ commands.build(walked);
99
+ // The region inverse: deselecting `skill` empties its regions instead of deleting the file
100
+ // the author writes prose in, and re-selecting refills the markers the strip preserved.
101
+ authorProse(walked);
102
+ setSurfaces(walked, end.filter((id) => id !== "skill"));
103
+ expect(commands.build(walked).result.stripped).toEqual(["skills/hello/SKILL.md"]);
104
+ const husk = readFileSync(join(walked, "skills/hello/SKILL.md"), "utf8");
105
+ expect(husk).toContain(PROSE);
106
+ expect(husk).toContain("<!-- tf:operations --><!-- /tf:operations -->");
107
+ expect(husk).not.toContain("## Operations");
108
+ expect(lstatSync(join(walked, ".agents/skills/hello"), { throwIfNoEntry: false })).toBeUndefined();
109
+ await expect(commands.check(walked)).resolves.toBeDefined();
110
+ setSurfaces(walked, end);
111
+ commands.build(walked);
112
+ commands.adopt(walked, "COVERAGE.md");
113
+ writeFileSync(join(walked, "COVERAGE.md"), "hand-written while adopted\n");
114
+ commands.build(walked);
115
+ commands.unadopt(walked, "COVERAGE.md");
116
+ commands.build(walked);
117
+ const fresh = scratch();
118
+ commands.init({ ...identity, root: fresh, surfaces: end, setup: false });
119
+ await commands.introspect(fresh);
120
+ authorProse(fresh);
121
+ commands.build(fresh);
122
+ expect(tree(walked)).toEqual(tree(fresh));
123
+ });
124
+ });
@@ -0,0 +1,10 @@
1
+ import type { Project } from "../model.js";
2
+ export declare const DRIFT_ENTRY: string;
3
+ /**
4
+ * Drift lines (empty means the generator still writes what BROWSER_SCAFFOLD pins) and the icons
5
+ * copied into the project because it had none.
6
+ */
7
+ export declare function mirrorScaffold(project: Project): {
8
+ drift: string[];
9
+ seeded: string[];
10
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The I/O half of the browser-extension surface: §8 C2, "mirror scaffolds by execution, not
3
+ * transcription". Runs the real `wxt init` into a temp directory and checks that everything it
4
+ * writes still agrees with BROWSER_SCAFFOLD, so an upstream template change is inherited on the
5
+ * next toolfactory release instead of silently rotting in a constant.
6
+ *
7
+ * The same run also seeds the project's placeholder icon set. Those five PNGs are WXT's binary
8
+ * files, not toolfactory's, and a pure `plan()` can only emit text — so they are copied out of
9
+ * the fresh scaffold when the project is missing them, exactly as the shadcn CLI (not the web
10
+ * surface) writes the components under `web/src/components/ui/`.
11
+ *
12
+ * Runnable as a script (`node dist/hosts/browser.js <root>`) because that is how the surface's
13
+ * `validate()` step reaches it: one Command like every other validator.
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { argv, exit, stderr, stdout } from "node:process";
20
+ import { fileURLToPath } from "node:url";
21
+ import { loadProject } from "../project/load.js";
22
+ import { BROWSER_SCAFFOLD, HOST_DIR, WXT_PIN } from "../surfaces/browser-extension.js";
23
+ export const DRIFT_ENTRY = fileURLToPath(import.meta.url);
24
+ /** Where WXT's auto-detection looks for the icon set, and therefore where the scaffold writes it. */
25
+ const ICON_DIR = "public/icon";
26
+ function differs(label, upstream, pinned) {
27
+ return JSON.stringify(upstream) === JSON.stringify(pinned)
28
+ ? []
29
+ : [`${label}: \`wxt init\` and BROWSER_SCAFFOLD no longer agree`];
30
+ }
31
+ /**
32
+ * Drift lines (empty means the generator still writes what BROWSER_SCAFFOLD pins) and the icons
33
+ * copied into the project because it had none.
34
+ */
35
+ export function mirrorScaffold(project) {
36
+ const directory = mkdtempSync(join(tmpdir(), "toolfactory-browser-"));
37
+ const probe = join(directory, "probe");
38
+ // Prefer the wxt the extension pins as a devDependency over a fresh download.
39
+ const pinned = join(project.root, HOST_DIR, "node_modules/.bin/wxt");
40
+ try {
41
+ execFileSync(existsSync(pinned) ? pinned : "npx", [
42
+ ...(existsSync(pinned) ? [] : ["--yes", `wxt@${WXT_PIN}`]),
43
+ "init",
44
+ probe,
45
+ "--template",
46
+ BROWSER_SCAFFOLD.template,
47
+ "--pm",
48
+ BROWSER_SCAFFOLD.packageManager,
49
+ ], { stdio: "pipe" });
50
+ const scaffold = (path) => readFileSync(join(probe, path), "utf8");
51
+ const packageJson = JSON.parse(scaffold("package.json"));
52
+ const icons = join(project.root, HOST_DIR, ICON_DIR);
53
+ const seeded = BROWSER_SCAFFOLD.icons.flatMap((size) => {
54
+ const name = `${size}.png`;
55
+ if (!existsSync(join(probe, ICON_DIR, name)))
56
+ return [];
57
+ if (existsSync(join(icons, name)))
58
+ return [];
59
+ mkdirSync(icons, { recursive: true });
60
+ copyFileSync(join(probe, ICON_DIR, name), join(icons, name));
61
+ return [`${ICON_DIR}/${name}`];
62
+ });
63
+ return {
64
+ drift: [
65
+ ...["scripts", "dependencies", "devDependencies"].flatMap((key) => differs(`package.json ${key}`, packageJson[key], BROWSER_SCAFFOLD.packageJson[key])),
66
+ ...(scaffold("tsconfig.json") === BROWSER_SCAFFOLD.tsconfig
67
+ ? []
68
+ : ["tsconfig.json: the template no longer writes what BROWSER_SCAFFOLD.tsconfig pins"]),
69
+ ...(scaffold(".gitignore") === BROWSER_SCAFFOLD.gitignore
70
+ ? []
71
+ : [".gitignore: the template no longer writes what BROWSER_SCAFFOLD.gitignore pins"]),
72
+ ...BROWSER_SCAFFOLD.icons.flatMap((size) => existsSync(join(probe, ICON_DIR, `${size}.png`))
73
+ ? []
74
+ : [`${ICON_DIR}/${size}.png: the template no longer ships this icon`]),
75
+ ],
76
+ seeded,
77
+ };
78
+ }
79
+ finally {
80
+ rmSync(directory, { recursive: true, force: true });
81
+ }
82
+ }
83
+ if (argv[1] === DRIFT_ENTRY) {
84
+ const { drift, seeded } = mirrorScaffold(loadProject(argv[2] ?? "."));
85
+ for (const path of seeded)
86
+ stdout.write(`copied WXT's ${path} into ${HOST_DIR}\n`);
87
+ if (drift.length) {
88
+ stderr.write(`the wxt scaffold drifted upstream; update BROWSER_SCAFFOLD in src/surfaces/browser-extension.ts:\n${drift
89
+ .map((line) => ` ${line}`)
90
+ .join("\n")}\n`);
91
+ exit(1);
92
+ }
93
+ stdout.write(`wxt init still matches src/surfaces/browser-extension.ts\n`);
94
+ }
@@ -0,0 +1,66 @@
1
+ import type { Project, SurfaceId } from "../model.js";
2
+ /** The GitHub environment the generated `live` job deploys to. */
3
+ export declare const LIVE_ENVIRONMENT = "live-tests";
4
+ export interface BootstrapOptions {
5
+ /** `owner/repo` to prepare; the default is the identity file's repository URL. */
6
+ repository?: string;
7
+ /** GitHub logins that must approve a live run. Empty leaves the environment ungated. */
8
+ reviewers?: string[];
9
+ /** Where the secret values come from; defaults to `<root>/.env`. */
10
+ envFile?: string;
11
+ /**
12
+ * Names the release registries consume, at repository scope — `registries(project)`'s union,
13
+ * passed in so this file stays a `gh` driver and the table stays in `project/gate.ts`.
14
+ */
15
+ releaseSecrets?: string[];
16
+ /** The one-time human steps to print at the end; `manualSteps(project)`'s list. */
17
+ manual?: string[];
18
+ /** Print the `gh` invocations instead of running them. */
19
+ dryRun?: boolean;
20
+ }
21
+ export interface BootstrapResult {
22
+ /** `owner/repo`, from the identity file's repository URL. */
23
+ repository: string;
24
+ environment: string;
25
+ reviewers: string[];
26
+ /** Names of the secrets written, in `gh` order. Never their values. */
27
+ secrets: string[];
28
+ /** Declared names with no value in `.env`: reported, never fatal — the rest of the run still lands. */
29
+ missing: string[];
30
+ /** Every `gh` invocation, in order, as it is (or would be) run. */
31
+ commands: string[];
32
+ /** The one-time human steps no API can do, each with its URL. */
33
+ manual: string[];
34
+ dryRun: boolean;
35
+ }
36
+ /** `owner/repo` from a GitHub URL or `git@` remote. */
37
+ export declare function githubSlug(repository: string | undefined): string | undefined;
38
+ export declare function bootstrapRepo(project: Project, options?: BootstrapOptions): BootstrapResult;
39
+ /**
40
+ * One GitHub topic per selected surface, so a repository `init` creates is discoverable as what
41
+ * it actually ships. Surfaces that only package the same tool (`cli`, `npm`, `pypi`, `web`,
42
+ * `clawhub`, `workflows`) name no ecosystem and get none.
43
+ */
44
+ export declare const SURFACE_TOPICS: Partial<Record<SurfaceId, string>>;
45
+ export interface CreateRepoOptions {
46
+ /** `owner/name` of the repository to create. */
47
+ slug: string;
48
+ /** Public instead of the private default. */
49
+ public?: boolean;
50
+ /** Print the `gh` invocations instead of running them. */
51
+ dryRun?: boolean;
52
+ }
53
+ export interface CreateRepoResult {
54
+ repository: string;
55
+ visibility: "private" | "public";
56
+ topics: string[];
57
+ /** Every `gh` invocation, in order, as it is (or would be) run. */
58
+ commands: string[];
59
+ dryRun: boolean;
60
+ }
61
+ /**
62
+ * Create the repository from the checkout and push it: `gh repo create --source --remote --push`,
63
+ * private unless asked otherwise, then one `gh repo edit --add-topic` for the selected surfaces.
64
+ * `gh` is the only GitHub client (§8 C1); nothing here talks to the API directly.
65
+ */
66
+ export declare function createRepo(project: Project, options: CreateRepoOptions): CreateRepoResult;