toolfactory 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/dist/bindings/python.js +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/distribution/native.d.ts +50 -0
- package/dist/distribution/native.js +414 -0
- package/dist/distribution/native.test.d.ts +1 -0
- package/dist/distribution/native.test.js +127 -0
- package/dist/distribution/system.d.ts +57 -0
- package/dist/distribution/system.js +637 -0
- package/dist/distribution/system.test.d.ts +1 -0
- package/dist/distribution/system.test.js +116 -0
- package/dist/e2e.test.js +7 -1
- package/dist/hosts/openclaw.js +6 -2
- package/dist/hosts/vscode.d.ts +1 -0
- package/dist/hosts/vscode.js +67 -0
- package/dist/model.d.ts +63 -2
- package/dist/model.js +18 -1
- package/dist/ops.d.ts +52 -0
- package/dist/project/apply.d.ts +1 -1
- package/dist/project/apply.js +194 -17
- package/dist/project/apply.test.js +103 -0
- package/dist/project/gate.d.ts +17 -2
- package/dist/project/gate.js +102 -23
- package/dist/project/history.d.ts +30 -1
- package/dist/project/lock.d.ts +5 -0
- package/dist/project/lock.js +9 -0
- package/dist/surfaces/agent-plugins.js +5 -4
- package/dist/surfaces/agents.js +27 -3
- package/dist/surfaces/agents.test.js +12 -0
- package/dist/surfaces/claude.js +8 -3
- package/dist/surfaces/codex.js +9 -4
- package/dist/surfaces/codex.test.js +16 -6
- package/dist/surfaces/cursor.js +2 -2
- package/dist/surfaces/distribution.test.js +121 -7
- package/dist/surfaces/dsh.js +29 -7
- package/dist/surfaces/dsh.test.js +11 -6
- package/dist/surfaces/gemini.js +2 -2
- package/dist/surfaces/gemini.test.js +4 -4
- package/dist/surfaces/hermes-native.js +43 -12
- package/dist/surfaces/hermes-native.test.js +53 -11
- package/dist/surfaces/mcp-registry.js +59 -7
- package/dist/surfaces/mcp-registry.test.js +23 -0
- package/dist/surfaces/mcpb.d.ts +1 -0
- package/dist/surfaces/mcpb.js +36 -23
- package/dist/surfaces/mcpb.test.js +31 -3
- package/dist/surfaces/npm.js +45 -14
- package/dist/surfaces/openclaw-native.js +10 -8
- package/dist/surfaces/openclaw-native.test.js +30 -0
- package/dist/surfaces/pypi.js +27 -3
- package/dist/surfaces/readme.d.ts +16 -0
- package/dist/surfaces/readme.js +17 -7
- package/dist/surfaces/registry.js +9 -3
- package/dist/surfaces/vscode-extension.d.ts +14 -0
- package/dist/surfaces/vscode-extension.js +312 -0
- package/dist/surfaces/vscode-extension.test.d.ts +1 -0
- package/dist/surfaces/vscode-extension.test.js +81 -0
- package/dist/surfaces/vscode-scaffold.d.ts +43 -0
- package/dist/surfaces/vscode-scaffold.js +43 -0
- package/dist/surfaces/workflows.js +57 -11
- package/dist/surfaces/workflows.test.js +71 -3
- package/dist/toolfactory/cli.js +1 -1
- package/dist/toolfactory/mcp.js +1 -1
- package/package.json +2 -1
- package/src/bindings/python.ts +1 -1
- package/src/distribution/native.test.ts +161 -0
- package/src/distribution/native.ts +460 -0
- package/src/distribution/system.test.ts +136 -0
- package/src/distribution/system.ts +688 -0
- package/src/e2e.test.ts +7 -1
- package/src/hosts/openclaw.ts +4 -2
- package/src/hosts/vscode.ts +79 -0
- package/src/model.ts +26 -2
- package/src/project/apply.test.ts +120 -0
- package/src/project/apply.ts +257 -15
- package/src/project/gate.ts +131 -24
- package/src/project/lock.ts +12 -0
- package/src/surfaces/agent-plugins.ts +5 -4
- package/src/surfaces/agents.test.ts +16 -0
- package/src/surfaces/agents.ts +38 -3
- package/src/surfaces/claude.ts +7 -3
- package/src/surfaces/codex.test.ts +15 -5
- package/src/surfaces/codex.ts +12 -6
- package/src/surfaces/cursor.ts +2 -10
- package/src/surfaces/distribution.test.ts +141 -6
- package/src/surfaces/dsh.test.ts +11 -5
- package/src/surfaces/dsh.ts +30 -7
- package/src/surfaces/gemini.test.ts +3 -3
- package/src/surfaces/gemini.ts +1 -2
- package/src/surfaces/hermes-native.test.ts +61 -10
- package/src/surfaces/hermes-native.ts +46 -13
- package/src/surfaces/mcp-registry.test.ts +28 -0
- package/src/surfaces/mcp-registry.ts +66 -8
- package/src/surfaces/mcpb.test.ts +32 -5
- package/src/surfaces/mcpb.ts +44 -23
- package/src/surfaces/npm.ts +47 -15
- package/src/surfaces/openclaw-native.test.ts +37 -0
- package/src/surfaces/openclaw-native.ts +49 -47
- package/src/surfaces/pypi.ts +27 -3
- package/src/surfaces/readme.ts +21 -6
- package/src/surfaces/registry.ts +9 -3
- package/src/surfaces/vscode-extension.test.ts +89 -0
- package/src/surfaces/vscode-extension.ts +345 -0
- package/src/surfaces/vscode-scaffold.ts +55 -0
- package/src/surfaces/workflows.test.ts +86 -3
- package/src/surfaces/workflows.ts +57 -11
- package/src/toolfactory/cli.ts +1 -1
- package/src/toolfactory/mcp.ts +1 -1
- package/web/dist/assets/{index-CCXhQKK3.css → index-BuT2czb2.css} +1 -1
- package/web/dist/assets/{index-D1PwZ3qS.js → index-CVm6m4s0.js} +1 -1
- package/web/dist/index.html +2 -2
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { stringify as yamlStringify } from "yaml";
|
|
21
21
|
import { LIVE_TEST_COMMAND } from "../bindings/python.js";
|
|
22
|
+
import { nativePublishJobs } from "../distribution/native.js";
|
|
23
|
+
import { systemPackageJobs, systemPublishJobs } from "../distribution/system.js";
|
|
22
24
|
import { githubOwner } from "../identity/name.js";
|
|
23
25
|
import { isInstructionOnly, } from "../model.js";
|
|
24
26
|
import { bootstrapSteps, gateSteps, gateVariable, MCP_PUBLISHER_FETCH, npmTarball, openclawTarball, outputsStep, PACKAGE_MANAGER_COMMANDS, packageSteps, RELEASE_DIR, RELEASE_SECRET_NAMES, registries, tagVersionAssert, toolfactoryCli, unpublishStep, WEB_BUILD, } from "../project/gate.js";
|
|
@@ -28,6 +30,7 @@ import { ociImage, registryName } from "./mcp-registry.js";
|
|
|
28
30
|
import { HOST_DIR as OPENCLAW_HOST_DIR } from "./openclaw-native.js";
|
|
29
31
|
import { compact, configProperties, envName, has, isSensitive, liveCredentials, npmName, requiredConfig, } from "./shared.js";
|
|
30
32
|
import { skillPath } from "./skill.js";
|
|
33
|
+
import { OVSX_VERSION, VSCE_VERSION, vsixName } from "./vscode-extension.js";
|
|
31
34
|
const YAML_HEADER = "# Generated by toolfactory. Do not edit; run `toolfactory build` to regenerate.\n\n";
|
|
32
35
|
/** The one artifact the `package` job uploads and every later job reads. */
|
|
33
36
|
const RELEASE_ARTIFACT = "release-assets";
|
|
@@ -182,6 +185,7 @@ function ciDocument(project) {
|
|
|
182
185
|
const node = typescript ? "${{ matrix.node-version }}" : "24";
|
|
183
186
|
const job = compact({
|
|
184
187
|
"runs-on": "ubuntu-latest",
|
|
188
|
+
env: { TOOLFACTORY_SYSTEM_NATIVE_JOBS: "true" },
|
|
185
189
|
strategy: typescript ? { matrix: { "node-version": ["22", "24"] } } : undefined,
|
|
186
190
|
steps: [
|
|
187
191
|
...toolchainSteps(project, node),
|
|
@@ -199,7 +203,7 @@ function ciDocument(project) {
|
|
|
199
203
|
workflow_dispatch: live ? {} : undefined,
|
|
200
204
|
}),
|
|
201
205
|
permissions: { contents: "read" },
|
|
202
|
-
jobs: compact({ test: job, live }),
|
|
206
|
+
jobs: compact({ test: job, live, ...systemPackageJobs(project, "${{ github.sha }}") }),
|
|
203
207
|
};
|
|
204
208
|
}
|
|
205
209
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -362,11 +366,15 @@ function releaseDocument(project) {
|
|
|
362
366
|
};
|
|
363
367
|
const cli = toolfactoryCli(project);
|
|
364
368
|
const assert = tagVersionAssert(project);
|
|
369
|
+
const systemJobs = systemPackageJobs(project, RELEASE_SHA);
|
|
370
|
+
for (const job of Object.values(systemJobs))
|
|
371
|
+
job.needs = "gate";
|
|
365
372
|
const jobs = {
|
|
373
|
+
...systemJobs,
|
|
366
374
|
gate: compact({
|
|
367
375
|
"runs-on": "ubuntu-latest",
|
|
368
376
|
permissions: compact({ contents: "read", pages: has(project, "web") ? "read" : undefined }),
|
|
369
|
-
env: { RELEASE_TAG: TAG_NAME },
|
|
377
|
+
env: { RELEASE_TAG: TAG_NAME, TOOLFACTORY_SYSTEM_NATIVE_JOBS: "true" },
|
|
370
378
|
outputs: { sha: "${{ steps.tag.outputs.sha }}", ...presenceOutputs(rows) },
|
|
371
379
|
steps: [
|
|
372
380
|
// The whole history: the tag may exist (a re-run) or not yet (a dispatch cuts it), and
|
|
@@ -385,7 +393,7 @@ function releaseDocument(project) {
|
|
|
385
393
|
],
|
|
386
394
|
}),
|
|
387
395
|
package: compact({
|
|
388
|
-
needs: "gate",
|
|
396
|
+
needs: needsOf(["gate", ...Object.keys(systemJobs)]),
|
|
389
397
|
"runs-on": "ubuntu-latest",
|
|
390
398
|
permissions: { contents: "read" },
|
|
391
399
|
// The Firefox self-hosted xpi packageSteps emits (gate.ts) is opt-in on this pair: present,
|
|
@@ -398,7 +406,11 @@ function releaseDocument(project) {
|
|
|
398
406
|
: undefined,
|
|
399
407
|
steps: [
|
|
400
408
|
...toolchainSteps(project, "24", RELEASE_SHA),
|
|
401
|
-
...packageSteps(project).map((step) => actionStep(step, false)),
|
|
409
|
+
...packageSteps(project, { system: false }).map((step) => actionStep(step, false)),
|
|
410
|
+
...Object.keys(systemJobs).map((name) => ({
|
|
411
|
+
uses: "actions/download-artifact@v8",
|
|
412
|
+
with: { name, path: `${RELEASE_DIR}/system/${name.replace(/^system-/, "")}` },
|
|
413
|
+
})),
|
|
402
414
|
{
|
|
403
415
|
uses: "actions/upload-artifact@v7",
|
|
404
416
|
with: { name: RELEASE_ARTIFACT, path: `${RELEASE_DIR}/` },
|
|
@@ -406,9 +418,14 @@ function releaseDocument(project) {
|
|
|
406
418
|
],
|
|
407
419
|
}),
|
|
408
420
|
};
|
|
409
|
-
|
|
421
|
+
Object.assign(jobs, systemPublishJobs(project, RELEASE_ARTIFACT, RELEASE_SHA));
|
|
422
|
+
const nativeJobs = nativePublishJobs(project, RELEASE_ARTIFACT, RELEASE_SHA);
|
|
423
|
+
Object.assign(jobs, nativeJobs);
|
|
424
|
+
const priorLegs = Object.keys(nativeJobs);
|
|
410
425
|
if (npmSelected) {
|
|
411
|
-
|
|
426
|
+
// npm treats a slash-containing bare operand as a Git spec; make the downloaded archive a
|
|
427
|
+
// filesystem path explicitly.
|
|
428
|
+
const tarball = `./${RELEASE_ARTIFACT}/${npmTarball(project)}`;
|
|
412
429
|
jobs["publish-npm"] = compact({
|
|
413
430
|
needs: ["gate", "package"],
|
|
414
431
|
if: gated("npm"),
|
|
@@ -442,18 +459,20 @@ function releaseDocument(project) {
|
|
|
442
459
|
}
|
|
443
460
|
if (pypiSelected) {
|
|
444
461
|
jobs["publish-pypi"] = compact({
|
|
445
|
-
needs: "gate",
|
|
462
|
+
needs: ["gate", "package"],
|
|
446
463
|
if: gated("pypi"),
|
|
447
464
|
"runs-on": "ubuntu-latest",
|
|
448
465
|
environment: "pypi",
|
|
449
466
|
permissions: { "id-token": "write" },
|
|
450
467
|
steps: [
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
468
|
+
{
|
|
469
|
+
uses: "actions/download-artifact@v8",
|
|
470
|
+
with: { name: RELEASE_ARTIFACT, path: RELEASE_ARTIFACT },
|
|
471
|
+
},
|
|
454
472
|
{
|
|
455
473
|
name: "Publish package distributions to PyPI",
|
|
456
474
|
uses: "pypa/gh-action-pypi-publish@release/v1",
|
|
475
|
+
with: { "packages-dir": `${RELEASE_ARTIFACT}/pypi/`, "skip-existing": true },
|
|
457
476
|
},
|
|
458
477
|
],
|
|
459
478
|
});
|
|
@@ -581,6 +600,33 @@ function releaseDocument(project) {
|
|
|
581
600
|
comments.push("", "publish-browser-ext-safari (opt-in, tool.json browserExtension.safari) is the macOS", "host-native leg: xcrun safari-web-extension-converter, then an Xcode archive/export", "signed with an App Store Connect API key. Runs on macos-latest, needs only `gate`, and", "does not gate the release — it submits to App Store Connect, not dist/release/.");
|
|
582
601
|
}
|
|
583
602
|
}
|
|
603
|
+
for (const id of ["vscode-marketplace", "open-vsx"]) {
|
|
604
|
+
const row = rows.find((entry) => entry.id === id);
|
|
605
|
+
if (!row)
|
|
606
|
+
continue;
|
|
607
|
+
const file = `./${RELEASE_ARTIFACT}/${vsixName(project)}`;
|
|
608
|
+
const publish = id === "vscode-marketplace"
|
|
609
|
+
? `npx --yes @vscode/vsce@${VSCE_VERSION} publish --packagePath ${file} --skip-duplicate`
|
|
610
|
+
: `npx --yes ovsx@${OVSX_VERSION} publish ${file} --skip-duplicate`;
|
|
611
|
+
const job = `publish-${id}`;
|
|
612
|
+
const runtimeJob = project.tool.binding === "python" ? "publish-pypi" : "publish-npm";
|
|
613
|
+
jobs[job] = {
|
|
614
|
+
needs: ["gate", "package", runtimeJob],
|
|
615
|
+
if: `!cancelled() && !contains(needs.*.result, 'failure') && ${gated(id)}`,
|
|
616
|
+
"runs-on": "ubuntu-latest",
|
|
617
|
+
permissions: { contents: "read" },
|
|
618
|
+
env: Object.fromEntries(row.secrets.map((key) => [key, `\${{ secrets.${key} }}`])),
|
|
619
|
+
steps: [
|
|
620
|
+
{ uses: "actions/setup-node@v7", with: { "node-version": "24" } },
|
|
621
|
+
{
|
|
622
|
+
uses: "actions/download-artifact@v8",
|
|
623
|
+
with: { name: RELEASE_ARTIFACT, path: RELEASE_ARTIFACT },
|
|
624
|
+
},
|
|
625
|
+
{ name: `Publish ${id}`, run: publish },
|
|
626
|
+
],
|
|
627
|
+
};
|
|
628
|
+
priorLegs.push(job);
|
|
629
|
+
}
|
|
584
630
|
jobs.release = {
|
|
585
631
|
// The tag's own release, after every leg: the assets are the same ones the registries got.
|
|
586
632
|
// Skipped legs must not skip it, only failed ones; hence the explicit condition.
|
|
@@ -616,7 +662,7 @@ function releaseDocument(project) {
|
|
|
616
662
|
// A tag the dispatch is cutting does not exist yet: the Release creates it here.
|
|
617
663
|
target_commitish: RELEASE_SHA,
|
|
618
664
|
generate_release_notes: true,
|
|
619
|
-
files: `${RELEASE_ARTIFACT}
|
|
665
|
+
files: `${RELEASE_ARTIFACT}/**/*`,
|
|
620
666
|
fail_on_unmatched_files: true,
|
|
621
667
|
},
|
|
622
668
|
},
|
|
@@ -120,6 +120,73 @@ describe("workflows", () => {
|
|
|
120
120
|
});
|
|
121
121
|
expect(steps.some((s) => s.run === "uv run --with pytest pytest -q")).toBe(true);
|
|
122
122
|
});
|
|
123
|
+
it("installs a native Python core into Hermes only in validation jobs", () => {
|
|
124
|
+
const python = project(["cli", "mcp", "hermes-native"], {
|
|
125
|
+
tool: {
|
|
126
|
+
...project(["cli"]).tool,
|
|
127
|
+
binding: "python",
|
|
128
|
+
surfaces: ["cli", "mcp", "hermes-native"],
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const files = emitted(python);
|
|
132
|
+
const ci = yamlParse(files[".github/workflows/ci.yml"]);
|
|
133
|
+
const release = yamlParse(files[".github/workflows/release.yml"]);
|
|
134
|
+
const command = 'uv pip install --python "$HOME/.hermes/hermes-agent/venv/bin/python" --editable .';
|
|
135
|
+
const ciRuns = ci.jobs.test.steps.map((step) => step.run);
|
|
136
|
+
const gateRuns = release.jobs.gate.steps.map((step) => step.run);
|
|
137
|
+
const packageRuns = release.jobs.package.steps.map((step) => step.run);
|
|
138
|
+
const ciValidate = ciRuns.findIndex((run) => run?.endsWith(" validate"));
|
|
139
|
+
const gateValidate = gateRuns.findIndex((run) => run?.endsWith(" validate"));
|
|
140
|
+
expect(ciRuns.indexOf(command)).toBeGreaterThan(ciRuns.indexOf("uv sync"));
|
|
141
|
+
expect(ciRuns.indexOf(command)).toBeLessThan(ciValidate);
|
|
142
|
+
expect(gateRuns.indexOf(command)).toBeGreaterThan(gateRuns.indexOf("uv sync"));
|
|
143
|
+
expect(gateRuns.indexOf(command)).toBeLessThan(gateValidate);
|
|
144
|
+
expect(packageRuns).not.toContain(command);
|
|
145
|
+
const withoutHermes = project(["cli", "mcp"], {
|
|
146
|
+
tool: { ...project(["cli"]).tool, binding: "python", surfaces: ["cli", "mcp"] },
|
|
147
|
+
});
|
|
148
|
+
const typescriptHermes = project(["cli", "mcp", "hermes-native"]);
|
|
149
|
+
expect(emitted(withoutHermes)[".github/workflows/ci.yml"]).not.toContain(command);
|
|
150
|
+
expect(emitted(typescriptHermes)[".github/workflows/ci.yml"]).not.toContain(command);
|
|
151
|
+
});
|
|
152
|
+
it("publishes the package job's prebuilt Python distributions", () => {
|
|
153
|
+
const python = project(["pypi", "web"], {
|
|
154
|
+
tool: { ...project([]).tool, binding: "python", surfaces: ["pypi", "web"] },
|
|
155
|
+
});
|
|
156
|
+
const release = yamlParse(emitted(python)[".github/workflows/release.yml"] ?? "");
|
|
157
|
+
const publish = release.jobs["publish-pypi"];
|
|
158
|
+
expect(publish.needs).toEqual(["gate", "package"]);
|
|
159
|
+
expect(publish.steps).toEqual([
|
|
160
|
+
{
|
|
161
|
+
uses: "actions/download-artifact@v8",
|
|
162
|
+
with: { name: "release-assets", path: "release-assets" },
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "Publish package distributions to PyPI",
|
|
166
|
+
uses: "pypa/gh-action-pypi-publish@release/v1",
|
|
167
|
+
with: { "packages-dir": "release-assets/pypi/", "skip-existing": true },
|
|
168
|
+
},
|
|
169
|
+
]);
|
|
170
|
+
expect(release.jobs.release.steps.at(-1)?.with.files).toBe("release-assets/**/*");
|
|
171
|
+
expect(packageSteps(python).find((step) => step.name === "web build")).toBeDefined();
|
|
172
|
+
expect(packageSteps(python).find((step) => step.name === "python distributions")?.run).toBe("uv build --out-dir dist/release/pypi");
|
|
173
|
+
const mcpbPython = project(["pypi", "mcp", "mcpb", "web"], {
|
|
174
|
+
tool: {
|
|
175
|
+
...project([]).tool,
|
|
176
|
+
binding: "python",
|
|
177
|
+
surfaces: ["pypi", "mcp", "mcpb", "web"],
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const mcpb = packageSteps(mcpbPython).find((step) => step.name === "MCPB bundle")?.run;
|
|
181
|
+
expect(packageSteps(mcpbPython).map((step) => step.name)).toEqual(expect.arrayContaining(["python distributions", "MCPB bundle"]));
|
|
182
|
+
expect(packageSteps(mcpbPython)
|
|
183
|
+
.map((step) => step.name)
|
|
184
|
+
.indexOf("python distributions")).toBeLessThan(packageSteps(mcpbPython)
|
|
185
|
+
.map((step) => step.name)
|
|
186
|
+
.indexOf("MCPB bundle"));
|
|
187
|
+
expect(mcpb).toContain("tar -xzf dist/release/pypi/*.tar.gz -C dist/mcpb --strip-components=1");
|
|
188
|
+
expect(mcpb).not.toContain("cp -R src dist/mcpb/src");
|
|
189
|
+
});
|
|
123
190
|
it("release.yml runs the legs in the forced order npm -> oci -> mcp-registry -> clawhub, each gated on what the gate decided", () => {
|
|
124
191
|
const files = emitted(project(["npm", "mcp-registry", "clawhub", "openclaw-native", "skill", "claude", "web", "mcp"], {
|
|
125
192
|
identity: {
|
|
@@ -265,8 +332,8 @@ describe("workflows", () => {
|
|
|
265
332
|
// configured publisher selects OIDC; otherwise the stored token is a fallback. A re-dispatch
|
|
266
333
|
// after adding either credential must not attempt to republish an immutable version.
|
|
267
334
|
expect(release.jobs["publish-npm"].steps.some((s) => s.run?.includes("npm view hello@0.1.0 version") &&
|
|
268
|
-
s.run?.includes('elif [ "$NPM_TRUSTED_PUBLISHER" = true ]; then npm publish release-assets/hello-0.1.0.tgz --access public') &&
|
|
269
|
-
s.run?.includes('NODE_AUTH_TOKEN="$NPM_TOKEN" npm publish release-assets/hello-0.1.0.tgz --access public'))).toBe(true);
|
|
335
|
+
s.run?.includes('elif [ "$NPM_TRUSTED_PUBLISHER" = true ]; then npm publish ./release-assets/hello-0.1.0.tgz --access public') &&
|
|
336
|
+
s.run?.includes('NODE_AUTH_TOKEN="$NPM_TOKEN" npm publish ./release-assets/hello-0.1.0.tgz --access public'))).toBe(true);
|
|
270
337
|
// The oci leg pushes the very image server.json's oci entry names.
|
|
271
338
|
const meta = release.jobs["publish-oci"].steps.find((s) => s.id === "meta");
|
|
272
339
|
expect(meta?.with?.images).toBe("ghcr.io/acme/hello");
|
|
@@ -465,6 +532,7 @@ describe("gate", () => {
|
|
|
465
532
|
"toolfactory validate",
|
|
466
533
|
"author checks",
|
|
467
534
|
"author tests",
|
|
535
|
+
"author integration tests",
|
|
468
536
|
"openclaw end-to-end (scripted model, no LLM key)",
|
|
469
537
|
]);
|
|
470
538
|
// `npm ci` and a global CLI install are the runner's job, not a checkout's: `toolfactory gate`
|
|
@@ -491,7 +559,7 @@ describe("gate", () => {
|
|
|
491
559
|
]);
|
|
492
560
|
const runs = packageSteps(target).map((step) => step.run);
|
|
493
561
|
expect(runs).toContain("npm pack --pack-destination dist/release");
|
|
494
|
-
expect(runs).toContain("uv build --out-dir dist/release");
|
|
562
|
+
expect(runs).toContain("uv build --out-dir dist/release/pypi");
|
|
495
563
|
expect(runs.some((run) => run.includes("npm pack ./hosts/openclaw"))).toBe(true);
|
|
496
564
|
expect(runs).toContain('set -- skills .claude-plugin; for path in LICENSE LICENSE.md LICENSE.txt NOTICE NOTICE.md NOTICE.txt; do if [ -f "$path" ]; then set -- "$@" "$path"; fi; done; zip -qr dist/release/hello-plugin.zip "$@"');
|
|
497
565
|
const webBuild = runs.findIndex((run) => run.includes("npm -C web run build"));
|
package/dist/toolfactory/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ const operations = [...authored, web];
|
|
|
11
11
|
const program = new Command()
|
|
12
12
|
.name("toolfactory")
|
|
13
13
|
.description("Build an agent tool once; ship it as Agent Skills, MCP servers, Agent Plugins / Claude / Codex / Cursor bundles, OpenClaw and Hermes plugins, CLIs, and packages, with the tests and releases to match.")
|
|
14
|
-
.version("0.
|
|
14
|
+
.version("0.2.0");
|
|
15
15
|
for (const op of operations) {
|
|
16
16
|
if (!serves(op, "cli"))
|
|
17
17
|
continue;
|
package/dist/toolfactory/mcp.js
CHANGED
|
@@ -30,7 +30,7 @@ const operations = [...authored, web];
|
|
|
30
30
|
const introspecting = Boolean(process.env.TOOLFACTORY_INTROSPECT);
|
|
31
31
|
/** Builds one server instance with every servable operation registered; called once for stdio, once per HTTP request. */
|
|
32
32
|
function createServer() {
|
|
33
|
-
const server = new McpServer({ name: "toolfactory", version: "0.
|
|
33
|
+
const server = new McpServer({ name: "toolfactory", version: "0.2.0" });
|
|
34
34
|
for (const op of operations) {
|
|
35
35
|
if (!introspecting && !serves(op, "mcp"))
|
|
36
36
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolfactory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Build an agent tool once; ship it as Agent Skills, MCP servers, Agent Plugins / Claude / Codex / Cursor bundles, OpenClaw and Hermes plugins, CLIs, and packages, with the tests and releases to match.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"check": "biome check . && tsc -p tsconfig.json --noEmit",
|
|
18
18
|
"format": "biome format --write .",
|
|
19
19
|
"test": "vitest run --passWithNoTests",
|
|
20
|
+
"test:integration": "node --import tsx scripts/check-vscode.mjs",
|
|
20
21
|
"toolfactory": "node --import tsx src/toolfactory/cli.ts",
|
|
21
22
|
"vendor:template": "node --import tsx src/hosts/template.ts --write"
|
|
22
23
|
},
|
package/src/bindings/python.ts
CHANGED
|
@@ -236,7 +236,7 @@ def env_route(token: "str | None") -> Route:
|
|
|
236
236
|
return `
|
|
237
237
|
|
|
238
238
|
def web_dist() -> "Path | None":
|
|
239
|
-
"""The
|
|
239
|
+
"""The wheel maps \`${WEB_DIR}/dist\` here at package time; a checkout has it in place."""
|
|
240
240
|
package = Path(__file__).resolve().parent.parent
|
|
241
241
|
for candidate in (package / "${WEB_DIR}", package.parent.parent / "${WEB_DIR}" / "dist"):
|
|
242
242
|
if candidate.is_dir():
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Project } from "../model.js";
|
|
3
|
+
import {
|
|
4
|
+
nativeInstallLines,
|
|
5
|
+
nativePackageConfigSchema,
|
|
6
|
+
nativePackageSteps,
|
|
7
|
+
nativePublishJobs,
|
|
8
|
+
nativeRegistryRows,
|
|
9
|
+
} from "./native.js";
|
|
10
|
+
|
|
11
|
+
function project(
|
|
12
|
+
entries: unknown[],
|
|
13
|
+
surfaces = entries.map((entry) => (entry as { id: string }).id),
|
|
14
|
+
): Project {
|
|
15
|
+
return {
|
|
16
|
+
root: "/repo",
|
|
17
|
+
tool: {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
identity: "package.json",
|
|
20
|
+
binding: "typescript",
|
|
21
|
+
surfaces: surfaces as unknown as Project["tool"]["surfaces"],
|
|
22
|
+
bundle: { runtime: "package" },
|
|
23
|
+
tests: { examples: {} },
|
|
24
|
+
nativePackages: entries,
|
|
25
|
+
} as unknown as Project["tool"],
|
|
26
|
+
identity: { name: "tool", version: "1.2.3", repository: "https://github.com/acme/tool" },
|
|
27
|
+
identityExtra: {},
|
|
28
|
+
operations: [],
|
|
29
|
+
toolfactoryVersion: "0.1.0",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const cargo = {
|
|
33
|
+
id: "cargo",
|
|
34
|
+
path: "native/cli",
|
|
35
|
+
identity: "Cargo.toml",
|
|
36
|
+
name: "acme-cli",
|
|
37
|
+
versionCommand: "cargo pkgid | sed 's/.*@//'",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe("native package configuration", () => {
|
|
41
|
+
it("requires the package path, authored identity, and registry identity", () => {
|
|
42
|
+
expect(() => nativePackageConfigSchema.parse({ id: "cargo", path: "native" })).toThrow();
|
|
43
|
+
expect(nativePackageConfigSchema.parse(cargo)).toEqual(cargo);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects a selected registry with no config or duplicate configuration", () => {
|
|
47
|
+
expect(() => nativePackageSteps(project([], ["cargo"]))).toThrow(/requires exactly one/);
|
|
48
|
+
expect(() => nativePackageSteps(project([cargo, cargo]))).toThrow(/repeats/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("packages authored crates and checks their version against the release", () => {
|
|
52
|
+
const steps = nativePackageSteps(project([cargo]));
|
|
53
|
+
expect(steps.map((step) => step.name)).toEqual([
|
|
54
|
+
"cargo version matches release",
|
|
55
|
+
"Cargo crate",
|
|
56
|
+
]);
|
|
57
|
+
expect(steps[0]?.run).toContain("= '1.2.3'");
|
|
58
|
+
expect(steps[1]?.run).toContain("cargo package --manifest-path 'native/cli/Cargo.toml'");
|
|
59
|
+
expect(steps[1]?.run).toContain("--target-dir 'dist/native-build/cargo'");
|
|
60
|
+
expect(steps[1]?.run).not.toContain("/repo");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("reads structured Cargo metadata and the raw MSBuild version", () => {
|
|
64
|
+
const cargoDefault = {
|
|
65
|
+
id: "cargo",
|
|
66
|
+
path: "native/cli",
|
|
67
|
+
identity: "Cargo.toml",
|
|
68
|
+
name: "acme-cli",
|
|
69
|
+
};
|
|
70
|
+
const nuget = {
|
|
71
|
+
id: "nuget",
|
|
72
|
+
path: "dotnet",
|
|
73
|
+
identity: "Tool.csproj",
|
|
74
|
+
name: "Acme.Tool",
|
|
75
|
+
};
|
|
76
|
+
const steps = nativePackageSteps(project([cargoDefault, nuget]));
|
|
77
|
+
expect(steps[0]?.run).toContain(
|
|
78
|
+
"cargo metadata --no-deps --format-version 1 --manifest-path 'Cargo.toml'",
|
|
79
|
+
);
|
|
80
|
+
expect(steps[0]?.run).toContain("JSON.parse(s).packages.find");
|
|
81
|
+
expect(steps[2]?.run).toContain("dotnet msbuild 'Tool.csproj' -nologo -getProperty:Version");
|
|
82
|
+
expect(steps[2]?.run).not.toContain("sed -n");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("leaves VCS-discovered Packagist and Go modules for the final release tag", () => {
|
|
86
|
+
const entries = [
|
|
87
|
+
{
|
|
88
|
+
id: "packagist",
|
|
89
|
+
path: ".",
|
|
90
|
+
identity: "composer.json",
|
|
91
|
+
name: "acme/tool",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "go-module",
|
|
95
|
+
path: ".",
|
|
96
|
+
identity: "go.mod",
|
|
97
|
+
name: "github.com/acme/tool",
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
expect(nativePackageSteps(project(entries))).toEqual([]);
|
|
101
|
+
expect(nativeInstallLines(project(entries))).toEqual([
|
|
102
|
+
"`composer require acme/tool`",
|
|
103
|
+
"`go get github.com/acme/tool@v1.2.3`",
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("uses direct upstream publishing commands and exposes only their actual credentials", () => {
|
|
108
|
+
const entries = [
|
|
109
|
+
cargo,
|
|
110
|
+
{
|
|
111
|
+
id: "nuget",
|
|
112
|
+
path: "dotnet",
|
|
113
|
+
identity: "Tool.csproj",
|
|
114
|
+
name: "Acme.Tool",
|
|
115
|
+
versionCommand: "dotnet msbuild -getProperty:Version Tool.csproj",
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
const jobs = nativePublishJobs(
|
|
119
|
+
project(entries),
|
|
120
|
+
"release-assets",
|
|
121
|
+
"${{ needs.gate.outputs.sha }}",
|
|
122
|
+
);
|
|
123
|
+
expect(jobs["publish-cargo"]?.steps).toContainEqual(
|
|
124
|
+
expect.objectContaining({ run: expect.stringContaining("cargo publish") }),
|
|
125
|
+
);
|
|
126
|
+
expect(jobs["publish-nuget"]?.steps).toContainEqual(
|
|
127
|
+
expect.objectContaining({ run: expect.stringContaining("dotnet nuget push") }),
|
|
128
|
+
);
|
|
129
|
+
expect(jobs["publish-nuget"]?.steps).toContainEqual(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
uses: "actions/setup-dotnet@v5",
|
|
132
|
+
with: { "dotnet-version": "8.0.x" },
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
expect(nativeRegistryRows(project(entries)).map((row) => row.secrets)).toEqual([
|
|
136
|
+
["CARGO_REGISTRY_TOKEN"],
|
|
137
|
+
["NUGET_API_KEY"],
|
|
138
|
+
]);
|
|
139
|
+
expect(jobs["publish-cargo"]?.needs).toEqual(["gate", "package"]);
|
|
140
|
+
expect(jobs["publish-cargo"]?.if).toBe("needs.gate.outputs.cargo == 'true'");
|
|
141
|
+
expect(jobs["publish-cargo"]?.steps).not.toContainEqual(
|
|
142
|
+
expect.objectContaining({ uses: "dtolnay/rust-toolchain@stable" }),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("builds a gem from its package directory before moving the resulting asset", () => {
|
|
147
|
+
const steps = nativePackageSteps(
|
|
148
|
+
project([
|
|
149
|
+
{
|
|
150
|
+
id: "rubygems",
|
|
151
|
+
path: "native/ruby",
|
|
152
|
+
identity: "acme.gemspec",
|
|
153
|
+
name: "acme",
|
|
154
|
+
versionCommand: "ruby -e 'puts 1.2.3'",
|
|
155
|
+
},
|
|
156
|
+
]),
|
|
157
|
+
);
|
|
158
|
+
expect(steps[1]?.run).toContain("(cd 'native/ruby' && gem build --strict 'acme.gemspec')");
|
|
159
|
+
expect(steps[1]?.run).toContain("mv 'native/ruby/acme-1.2.3.gem'");
|
|
160
|
+
});
|
|
161
|
+
});
|