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
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ generated and every surface still installs, builds and publishes.
|
|
|
22
22
|
- **Gemini CLI extension** — `gemini extensions install https://github.com/GoatInAHat/toolfactory`
|
|
23
23
|
- **OpenClaw plugin** — `openclaw plugins install --link hosts/openclaw` from a checkout
|
|
24
24
|
- **Hermes plugin** — `hermes plugins install https://github.com/GoatInAHat/toolfactory#hosts/hermes/toolfactory_hermes`
|
|
25
|
-
- **DSH plugin** (experimental) — `dsh plugin --profile <profile> add ./hosts/dsh` from a checkout, or the release tarball `toolfactory-dsh-0.
|
|
25
|
+
- **DSH plugin** (experimental) — `dsh plugin --profile <profile> add ./hosts/dsh` from a checkout, or the release tarball `toolfactory-dsh-0.2.0.tgz`
|
|
26
26
|
- **Web app** — `npx -y toolfactory mcp --http --open` serves the operations page beside the
|
|
27
27
|
MCP endpoint on one port and opens it; over MCP or a skill, the `web` operation does the same and
|
|
28
28
|
returns the URL.
|
|
@@ -55,8 +55,7 @@ None of it is required: with no GitHub, plain git or no git at all, everything b
|
|
|
55
55
|
Use `--runtime none --surfaces skill,codex` when a plugin is only SKILL.md instructions for
|
|
56
56
|
Codex. ToolFactory then emits no MCP server, operation snapshot, language scaffold, or consumer
|
|
57
57
|
runtime dependency. `binding` remains required for config compatibility but is unused in this
|
|
58
|
-
mode.
|
|
59
|
-
than resolve the published npm package.
|
|
58
|
+
mode. This is available in the published npm package starting with ToolFactory 0.1.1.
|
|
60
59
|
|
|
61
60
|
1. Write operations in `src/ops.ts` (TypeScript) or `src/<pkg>/ops.py` (Python). Each one is a
|
|
62
61
|
name, a description, an input schema, an optional output schema, an optional `requires`
|
|
@@ -121,16 +120,39 @@ and an MCP server cannot give it (a gateway tab, browser capability, a store lis
|
|
|
121
120
|
| `gemini` | root `gemini-extension.json` — a Gemini CLI extension installable straight from the repo, reading your `AGENTS.md` and `skills/` | `gemini extensions validate` |
|
|
122
121
|
| `mcp`, `cli` | the kernel MCP server (stdio, or `--http`) and a CLI over your operations; each lists only the operations it can run | MCP Inspector, `--help` |
|
|
123
122
|
| `npm`, `pypi` | package metadata merged into `package.json` / `pyproject.toml` | `npm pack`, `uv build` |
|
|
123
|
+
| `cargo`, `nuget`, `maven-central`, `rubygems`, `packagist`, `go-module` | releases for author-owned native packages alongside the tool; metadata stays in its native format | Cargo, .NET, Maven, RubyGems, Composer, Go |
|
|
124
|
+
| `homebrew`, `winget`, `scoop`, `chocolatey`, `apt`, `rpm` | native packaging and catalog/repository distribution on the appropriate OS | the manager's own tooling |
|
|
124
125
|
| `mcp-registry` | `server.json` | `mcp-publisher` |
|
|
125
126
|
| `mcpb` | `hosts/mcpb/manifest.json` and a `.mcpb` release asset packed from the npm tarball — the one-click install Claude Desktop takes | `mcpb validate` |
|
|
126
127
|
| `openclaw-native` | `hosts/openclaw/`, mirroring `openclaw plugins init --type tool` | `openclaw plugins build --check`, `validate`, plugin-inspector |
|
|
127
128
|
| `hermes-native` | `hosts/hermes/`, a manifest v2 plugin | `hermes plugins doctor --ci` |
|
|
128
129
|
| `web` | `web/`, a shadcn/ui (Vite, React, Tailwind) app with a form per operation; your own pages sit beside it in `App.tsx` | `vite build`, Playwright |
|
|
129
130
|
| `browser-extension` | `hosts/browser/`, one WXT extension built for Chromium, Firefox and Safari: the worker calls your kernel over loopback MCP, the popup is the `web` app, and the operations that need a page are yours to write in `entrypoints/` | `wxt build`, `web-ext lint`, Playwright against a real Chromium |
|
|
131
|
+
| `vscode-extension` | `hosts/vscode/`, a native TypeScript extension with MCP discovery, command palette operations, settings and SecretStorage; activation and native contributions are yours | Microsoft `generator-code` scaffold comparison, `vsce package`, real Extension Development Host |
|
|
130
132
|
| `dsh` (experimental) | `hosts/dsh/`, a zero-code DSH (DeepSeek Harness) bundle: one Cordis patch row attaching your MCP server through `@deepseek-ai/dsh-mcp-client` | a keyless `dsh --profile headless` boot |
|
|
131
133
|
| workflows (always) | `ci.yml`, `release.yml` (gate → package → publish legs → GitHub Release, plus Pages), `compose.toolfactory.yaml`, `.env.example`, `renovate.json`; every step is one `toolfactory gate` / `toolfactory package` runs without GitHub | the workflow itself |
|
|
132
134
|
| readme (always) | the Install section of `README.md` (a marked region): one install line per selected surface, plus a static Agent Skill badge | — |
|
|
133
135
|
|
|
136
|
+
Select `vscode-extension` together with `mcp`, `cli`, and the runtime registry (`npm` for
|
|
137
|
+
TypeScript, `pypi` for Python). Set `vscode.publisher` in `dev.toolfactory/tool.json`, then build.
|
|
138
|
+
Open `hosts/vscode` and press F5 to use the checkout's runtime, or run `npm run vsix` in that
|
|
139
|
+
directory to package it. Native commands, views, webviews, language services, chat participants,
|
|
140
|
+
and language-model tools use the full VS Code API in your own `src/extension.ts`. Add contribution
|
|
141
|
+
points directly to its `package.json`; rebuilds preserve author entries and additional fields
|
|
142
|
+
on generated entries. Marketplace and Open VSX release jobs use `VSCE_PAT` and `OVSX_PAT` after
|
|
143
|
+
you register the publisher. Both can be disabled individually in `tool.json`.
|
|
144
|
+
|
|
145
|
+
The same native customization rule applies to the other surfaces: JSON, TOML and YAML manifests
|
|
146
|
+
merge only projected fields, and host entry points retain your code outside the marked regions.
|
|
147
|
+
OpenClaw, Hermes, DSH and browser shims can use their host's complete native API. `adopt` and
|
|
148
|
+
`eject` remain available when replacing a generated integration entirely.
|
|
149
|
+
|
|
150
|
+
Package distribution is opt-in. PyPI supports both `pip install` and `uv add`; pip uses the same
|
|
151
|
+
published wheel/sdist. Other ecosystems consume real native packages and metadata supplied by the
|
|
152
|
+
author. See [native package releases](docs/native-package-releases.md) and
|
|
153
|
+
[system package releases](docs/system-package-releases.md) for configuration and registry setup.
|
|
154
|
+
Toolfactory itself continues to use npm as its package registry.
|
|
155
|
+
|
|
134
156
|
## Driving toolfactory from an agent
|
|
135
157
|
|
|
136
158
|
Every command is an MCP tool, so a host can drive the whole loop without a shell — and nothing is
|
|
@@ -148,6 +170,12 @@ Inside OpenClaw, install the tool you are building with
|
|
|
148
170
|
`hermes plugins install file://<repo>#hosts/hermes/<pkg>` — every `hermes` run is a fresh process,
|
|
149
171
|
and `hermes gateway restart` is only for the messaging gateway.
|
|
150
172
|
|
|
173
|
+
GSD uses the same skills, CLI and MCP server; no GSD-specific surface is needed. From a generated
|
|
174
|
+
project, run `bash .agents/setup --all` so `.mcp.json` is rendered even on a GSD-only machine,
|
|
175
|
+
then discover its server in an interactive GSD session before unattended use. GSD launched by
|
|
176
|
+
OpenClaw or Hermes loads the child project's configuration separately from the outer agent.
|
|
177
|
+
See [GSD compatibility](docs/gsd.md) for setup, runtime boundaries and the repeatable smoke test.
|
|
178
|
+
|
|
151
179
|
## The boundary
|
|
152
180
|
|
|
153
181
|
Core logic is a pure function of JSON arguments, environment/config and the filesystem. An
|
package/dist/bindings/python.js
CHANGED
|
@@ -210,7 +210,7 @@ def env_route(token: "str | None") -> Route:
|
|
|
210
210
|
return `
|
|
211
211
|
|
|
212
212
|
def web_dist() -> "Path | None":
|
|
213
|
-
"""The
|
|
213
|
+
"""The wheel maps \`${WEB_DIR}/dist\` here at package time; a checkout has it in place."""
|
|
214
214
|
package = Path(__file__).resolve().parent.parent
|
|
215
215
|
for candidate in (package / "${WEB_DIR}", package.parent.parent / "${WEB_DIR}" / "dist"):
|
|
216
216
|
if candidate.is_dir():
|
package/dist/commands.d.ts
CHANGED
|
@@ -185,4 +185,4 @@ export declare function bootstrapRepo(args: {
|
|
|
185
185
|
dryRun?: boolean;
|
|
186
186
|
}): BootstrapResult;
|
|
187
187
|
export declare function doctor(): DoctorReport;
|
|
188
|
-
export declare const SURFACES: readonly ["skill", "agent-plugins", "claude", "codex", "cursor", "mcp", "mcp-registry", "cli", "npm", "pypi", "openclaw-native", "hermes-native", "clawhub", "web", "dsh", "workflows", "agents", "readme", "gemini", "mcpb", "browser-extension"];
|
|
188
|
+
export declare const SURFACES: readonly ["skill", "agent-plugins", "claude", "codex", "cursor", "mcp", "mcp-registry", "cli", "npm", "pypi", "openclaw-native", "hermes-native", "clawhub", "web", "dsh", "workflows", "agents", "readme", "gemini", "mcpb", "browser-extension", "vscode-extension", "cargo", "nuget", "maven-central", "rubygems", "packagist", "go-module", "homebrew", "winget", "scoop", "chocolatey", "apt", "rpm"];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native package registries are deliberately an escape hatch, not bindings. A project may
|
|
3
|
+
* carry a real Cargo/.NET/JVM/Ruby/PHP/Go package beside a Toolfactory TypeScript or Python
|
|
4
|
+
* kernel, but Toolfactory never translates that kernel into another language or invents a
|
|
5
|
+
* launcher to enter a registry.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { Project, Surface } from "../model.js";
|
|
9
|
+
import type { GateStep, Registry } from "../project/gate.js";
|
|
10
|
+
export declare const NATIVE_PACKAGE_IDS: readonly ["cargo", "nuget", "maven-central", "rubygems", "packagist", "go-module"];
|
|
11
|
+
export type NativePackageId = (typeof NATIVE_PACKAGE_IDS)[number];
|
|
12
|
+
/**
|
|
13
|
+
* `name` is intentionally not derived from the Toolfactory identity: package namespaces have
|
|
14
|
+
* their own ownership rules. A standard per-ecosystem version read is used unless the author
|
|
15
|
+
* needs a `versionCommand` escape hatch.
|
|
16
|
+
*/
|
|
17
|
+
export declare const nativePackageConfigSchema: z.ZodObject<{
|
|
18
|
+
id: z.ZodEnum<{
|
|
19
|
+
cargo: "cargo";
|
|
20
|
+
nuget: "nuget";
|
|
21
|
+
"maven-central": "maven-central";
|
|
22
|
+
rubygems: "rubygems";
|
|
23
|
+
packagist: "packagist";
|
|
24
|
+
"go-module": "go-module";
|
|
25
|
+
}>;
|
|
26
|
+
path: z.ZodString;
|
|
27
|
+
identity: z.ZodString;
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
versionCommand: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strict>;
|
|
31
|
+
export type NativePackageConfig = z.infer<typeof nativePackageConfigSchema>;
|
|
32
|
+
/** External registries do not generate files: their package metadata remains author-owned. */
|
|
33
|
+
export declare const nativePackageSurfaces: Surface[];
|
|
34
|
+
/**
|
|
35
|
+
* Release packaging happens after the main package build. VCS registries deliberately have no
|
|
36
|
+
* artifact: their published object is the signed Git tag and repository contents.
|
|
37
|
+
*/
|
|
38
|
+
export declare function nativePackageSteps(project: Project): GateStep[];
|
|
39
|
+
/**
|
|
40
|
+
* Kept with the release contract so local `gate` and the package job fail before a package
|
|
41
|
+
* command with a clear missing-tool message. GitHub's hosted Linux image supplies these upstream
|
|
42
|
+
* clients; callers that use another runner can install its normal distro packages first.
|
|
43
|
+
*/
|
|
44
|
+
export declare function nativeToolchainSteps(project: Project): GateStep[];
|
|
45
|
+
export declare function nativeRegistryRows(project: Project): Registry[];
|
|
46
|
+
type Job = Record<string, unknown>;
|
|
47
|
+
/** GitHub Actions jobs use the upstream language tool and package client directly. */
|
|
48
|
+
export declare function nativePublishJobs(project: Project, artifactDir: string, releaseSha: string): Record<string, Job>;
|
|
49
|
+
export declare function nativeInstallLines(project: Project): string[];
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native package registries are deliberately an escape hatch, not bindings. A project may
|
|
3
|
+
* carry a real Cargo/.NET/JVM/Ruby/PHP/Go package beside a Toolfactory TypeScript or Python
|
|
4
|
+
* kernel, but Toolfactory never translates that kernel into another language or invents a
|
|
5
|
+
* launcher to enter a registry.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
export const NATIVE_PACKAGE_IDS = [
|
|
9
|
+
"cargo",
|
|
10
|
+
"nuget",
|
|
11
|
+
"maven-central",
|
|
12
|
+
"rubygems",
|
|
13
|
+
"packagist",
|
|
14
|
+
"go-module",
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* `name` is intentionally not derived from the Toolfactory identity: package namespaces have
|
|
18
|
+
* their own ownership rules. A standard per-ecosystem version read is used unless the author
|
|
19
|
+
* needs a `versionCommand` escape hatch.
|
|
20
|
+
*/
|
|
21
|
+
export const nativePackageConfigSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
id: z.enum(NATIVE_PACKAGE_IDS),
|
|
24
|
+
path: z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1)
|
|
27
|
+
.refine((path) => !/^(?:[A-Za-z]:|[\\/])/.test(path) && !path.split(/[\\/]/).includes(".."), "path must stay inside the repository."),
|
|
28
|
+
identity: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.refine((path) => !/^(?:[A-Za-z]:|[\\/])/.test(path) && !path.split(/[\\/]/).includes(".."), "identity must be relative to path."),
|
|
32
|
+
name: z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.describe("Registry package identity, including its required namespace."),
|
|
36
|
+
versionCommand: z
|
|
37
|
+
.string()
|
|
38
|
+
.min(1)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Command run in path that prints exactly the authored native package version."),
|
|
41
|
+
})
|
|
42
|
+
.superRefine((entry, context) => {
|
|
43
|
+
if (entry.id === "maven-central" && entry.name.split(":").length !== 2)
|
|
44
|
+
context.addIssue({
|
|
45
|
+
code: "custom",
|
|
46
|
+
path: ["name"],
|
|
47
|
+
message: "Maven Central name must be groupId:artifactId.",
|
|
48
|
+
});
|
|
49
|
+
if ((entry.id === "packagist" || entry.id === "go-module") && entry.path !== ".")
|
|
50
|
+
context.addIssue({
|
|
51
|
+
code: "custom",
|
|
52
|
+
path: ["path"],
|
|
53
|
+
message: `${entry.id} is VCS/tag-discovered and currently requires package path ".".`,
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
.strict();
|
|
57
|
+
function configs(project) {
|
|
58
|
+
const raw = project.tool.nativePackages ?? [];
|
|
59
|
+
const parsed = raw.map((entry) => nativePackageConfigSchema.parse(entry));
|
|
60
|
+
const duplicates = parsed.filter((entry, index) => parsed.findIndex((other) => other.id === entry.id) !== index);
|
|
61
|
+
if (duplicates.length)
|
|
62
|
+
throw new Error(`Native package configuration repeats: ${[...new Set(duplicates.map((entry) => entry.id))].join(", ")}.`);
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
function selected(project, id) {
|
|
66
|
+
return project.tool.surfaces.includes(id);
|
|
67
|
+
}
|
|
68
|
+
function shellQuote(value) {
|
|
69
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
70
|
+
}
|
|
71
|
+
function defaultVersionCommand(entry) {
|
|
72
|
+
const identity = shellQuote(entry.identity);
|
|
73
|
+
switch (entry.id) {
|
|
74
|
+
case "cargo":
|
|
75
|
+
return `cargo metadata --no-deps --format-version 1 --manifest-path ${identity} | node -e ${shellQuote(`let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).packages.find(p=>p.name===${JSON.stringify(entry.name)}).version))`)}`;
|
|
76
|
+
case "nuget":
|
|
77
|
+
return `dotnet msbuild ${identity} -nologo -getProperty:Version`;
|
|
78
|
+
case "maven-central":
|
|
79
|
+
return `mvn --quiet --file ${identity} -DforceStdout help:evaluate -Dexpression=project.version`;
|
|
80
|
+
case "rubygems":
|
|
81
|
+
return `ruby -e 'puts Gem::Specification.load(ARGV[0]).version' ${identity}`;
|
|
82
|
+
case "packagist":
|
|
83
|
+
case "go-module":
|
|
84
|
+
throw new Error(`${entry.id} is discovered from the release tag and has no pre-release version command.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function versionAssert(entry, version) {
|
|
88
|
+
if (!version)
|
|
89
|
+
throw new Error(`Native package "${entry.name}" requires the shared identity to declare version.`);
|
|
90
|
+
return `test "$(${entry.versionCommand ?? defaultVersionCommand(entry)})" = ${shellQuote(version)}`;
|
|
91
|
+
}
|
|
92
|
+
function configFor(project, id) {
|
|
93
|
+
const matches = configs(project).filter((entry) => entry.id === id);
|
|
94
|
+
if (matches.length !== 1)
|
|
95
|
+
throw new Error(`Surface "${id}" requires exactly one tool.nativePackages entry with id "${id}".`);
|
|
96
|
+
const entry = matches[0];
|
|
97
|
+
if ((id === "packagist" || id === "go-module") && !project.identity.repository)
|
|
98
|
+
throw new Error(`Surface "${id}" requires identity.repository because it is discovered from the final Git tag.`);
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
function activeConfigs(project) {
|
|
102
|
+
return NATIVE_PACKAGE_IDS.filter((id) => selected(project, id)).map((id) => configFor(project, id));
|
|
103
|
+
}
|
|
104
|
+
function entryVersion(project, entry) {
|
|
105
|
+
if (!project.identity.version)
|
|
106
|
+
throw new Error(`Native package "${entry.name}" requires the shared identity to declare version.`);
|
|
107
|
+
return project.identity.version;
|
|
108
|
+
}
|
|
109
|
+
const commands = {
|
|
110
|
+
cargo: (entry) => [
|
|
111
|
+
{
|
|
112
|
+
label: "Cargo package",
|
|
113
|
+
command: "cargo",
|
|
114
|
+
args: ["package", "--manifest-path", entry.identity],
|
|
115
|
+
cwd: entry.path,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
nuget: (entry) => [
|
|
119
|
+
{
|
|
120
|
+
label: "NuGet pack",
|
|
121
|
+
command: "dotnet",
|
|
122
|
+
args: ["pack", entry.identity, "--configuration", "Release"],
|
|
123
|
+
cwd: entry.path,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
"maven-central": (entry) => [
|
|
127
|
+
{
|
|
128
|
+
label: "Maven verify",
|
|
129
|
+
command: "mvn",
|
|
130
|
+
args: ["--batch-mode", "--file", entry.identity, "verify"],
|
|
131
|
+
cwd: entry.path,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
rubygems: (entry) => [
|
|
135
|
+
{
|
|
136
|
+
label: "RubyGems build",
|
|
137
|
+
command: "gem",
|
|
138
|
+
args: ["build", "--strict", entry.identity],
|
|
139
|
+
cwd: entry.path,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
packagist: (entry) => [
|
|
143
|
+
{
|
|
144
|
+
label: "Composer validate",
|
|
145
|
+
command: "composer",
|
|
146
|
+
args: ["validate", "--strict", entry.identity],
|
|
147
|
+
cwd: entry.path,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
"go-module": (entry) => [
|
|
151
|
+
{ label: "Go module tidy", command: "go", args: ["mod", "tidy", "-diff"], cwd: entry.path },
|
|
152
|
+
{ label: "Go module tests", command: "go", args: ["test", "./..."], cwd: entry.path },
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
/** External registries do not generate files: their package metadata remains author-owned. */
|
|
156
|
+
export const nativePackageSurfaces = NATIVE_PACKAGE_IDS.map((id) => ({
|
|
157
|
+
id,
|
|
158
|
+
plan(project) {
|
|
159
|
+
configFor(project, id);
|
|
160
|
+
return [];
|
|
161
|
+
},
|
|
162
|
+
validate(project) {
|
|
163
|
+
const entry = configFor(project, id);
|
|
164
|
+
return commands[id](entry).map((command) => ({ ...command, cwd: entry.path }));
|
|
165
|
+
},
|
|
166
|
+
}));
|
|
167
|
+
/**
|
|
168
|
+
* Release packaging happens after the main package build. VCS registries deliberately have no
|
|
169
|
+
* artifact: their published object is the signed Git tag and repository contents.
|
|
170
|
+
*/
|
|
171
|
+
export function nativePackageSteps(project) {
|
|
172
|
+
return activeConfigs(project).flatMap((entry) => {
|
|
173
|
+
// Packagist and Go proxy discovery happens from the tag created by the final release job.
|
|
174
|
+
// Before then, `git describe` can only see the prior version, so asserting it here is wrong.
|
|
175
|
+
if (entry.id === "packagist" || entry.id === "go-module")
|
|
176
|
+
return [];
|
|
177
|
+
const directory = shellQuote(entry.path);
|
|
178
|
+
const out = `dist/release/native/${entry.id}`;
|
|
179
|
+
const check = versionAssert(entry, project.identity.version);
|
|
180
|
+
const steps = [
|
|
181
|
+
{ name: `${entry.id} version matches release`, run: `cd ${directory} && ${check}` },
|
|
182
|
+
];
|
|
183
|
+
switch (entry.id) {
|
|
184
|
+
case "cargo":
|
|
185
|
+
steps.push({
|
|
186
|
+
name: "Cargo crate",
|
|
187
|
+
run: `mkdir -p ${shellQuote(out)} && cargo package --manifest-path ${shellQuote(`${entry.path}/${entry.identity}`)} --target-dir ${shellQuote(`dist/native-build/${entry.id}`)} && cp ${shellQuote(`dist/native-build/${entry.id}/package`)}/*.crate ${shellQuote(out)}/`,
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
case "nuget":
|
|
191
|
+
steps.push({
|
|
192
|
+
name: "NuGet package",
|
|
193
|
+
run: `mkdir -p ${shellQuote(out)} && dotnet pack ${shellQuote(`${entry.path}/${entry.identity}`)} --configuration Release --output ${shellQuote(out)}`,
|
|
194
|
+
});
|
|
195
|
+
break;
|
|
196
|
+
case "maven-central":
|
|
197
|
+
steps.push({
|
|
198
|
+
name: "Maven package",
|
|
199
|
+
run: `mkdir -p ${shellQuote(out)} && mvn --batch-mode --file ${shellQuote(`${entry.path}/${entry.identity}`)} package && cp ${shellQuote(`${entry.path}/${entry.identity}`)} ${shellQuote(`${out}/${entry.name.split(":")[1]}-${project.identity.version}.pom`)} && find ${shellQuote(`${entry.path}/target`)} -maxdepth 1 -type f \\( -name '*.jar' -o -name '*.war' -o -name '*.pom' \\) -exec cp {} ${shellQuote(out)} \\;`,
|
|
200
|
+
});
|
|
201
|
+
break;
|
|
202
|
+
case "rubygems":
|
|
203
|
+
steps.push({
|
|
204
|
+
name: "Ruby gem",
|
|
205
|
+
run: `mkdir -p ${shellQuote(out)} && (cd ${directory} && gem build --strict ${shellQuote(entry.identity)}) && mv ${shellQuote(`${entry.path}/${entry.name}-${project.identity.version}.gem`)} ${shellQuote(out)}/`,
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
return steps;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Kept with the release contract so local `gate` and the package job fail before a package
|
|
214
|
+
* command with a clear missing-tool message. GitHub's hosted Linux image supplies these upstream
|
|
215
|
+
* clients; callers that use another runner can install its normal distro packages first.
|
|
216
|
+
*/
|
|
217
|
+
export function nativeToolchainSteps(project) {
|
|
218
|
+
const tools = {
|
|
219
|
+
cargo: "cargo",
|
|
220
|
+
nuget: "dotnet",
|
|
221
|
+
"maven-central": "mvn",
|
|
222
|
+
rubygems: "gem",
|
|
223
|
+
packagist: "composer",
|
|
224
|
+
"go-module": "go",
|
|
225
|
+
};
|
|
226
|
+
return activeConfigs(project).map((entry) => ({
|
|
227
|
+
name: `${entry.id} upstream toolchain`,
|
|
228
|
+
run: `command -v ${tools[entry.id]} >/dev/null || { echo "${entry.id}: install its upstream ${tools[entry.id]} CLI before packaging" >&2; exit 1; }`,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
function row(project, entry) {
|
|
232
|
+
const version = entryVersion(project, entry);
|
|
233
|
+
const [groupId = "", artifactId = ""] = entry.name.split(":");
|
|
234
|
+
const mavenSearch = `https://search.maven.org/solrsearch/select?q=${encodeURIComponent(`g:"${groupId}" AND a:"${artifactId}" AND v:"${version}"`)}&rows=1&wt=json`;
|
|
235
|
+
const shared = {
|
|
236
|
+
id: entry.id,
|
|
237
|
+
surfaces: [entry.id],
|
|
238
|
+
url: {
|
|
239
|
+
cargo: "https://crates.io/settings/tokens",
|
|
240
|
+
nuget: "https://www.nuget.org/account/apikeys",
|
|
241
|
+
"maven-central": "https://central.sonatype.com/account",
|
|
242
|
+
rubygems: "https://rubygems.org/profile/edit",
|
|
243
|
+
packagist: "https://packagist.org/about",
|
|
244
|
+
"go-module": "https://go.dev/doc/modules/publishing",
|
|
245
|
+
}[entry.id],
|
|
246
|
+
};
|
|
247
|
+
switch (entry.id) {
|
|
248
|
+
case "cargo":
|
|
249
|
+
return {
|
|
250
|
+
...shared,
|
|
251
|
+
secrets: ["CARGO_REGISTRY_TOKEN"],
|
|
252
|
+
exists: `curl -fsS -o /dev/null ${shellQuote(`https://crates.io/api/v1/crates/${encodeURIComponent(entry.name)}/${encodeURIComponent(version)}`)}`,
|
|
253
|
+
gate: '[ -n "$CARGO_REGISTRY_TOKEN" ]',
|
|
254
|
+
retract: `cargo yank ${shellQuote(entry.name)} --vers ${shellQuote(version)} --token "$CARGO_REGISTRY_TOKEN"`,
|
|
255
|
+
};
|
|
256
|
+
case "nuget":
|
|
257
|
+
return {
|
|
258
|
+
...shared,
|
|
259
|
+
secrets: ["NUGET_API_KEY"],
|
|
260
|
+
exists: `curl -fsS -o /dev/null ${shellQuote(`https://api.nuget.org/v3-flatcontainer/${entry.name.toLowerCase()}/${version.toLowerCase()}/${entry.name.toLowerCase()}.${version.toLowerCase()}.nupkg`)}`,
|
|
261
|
+
gate: '[ -n "$NUGET_API_KEY" ]',
|
|
262
|
+
retract: `dotnet nuget delete ${shellQuote(entry.name)} ${shellQuote(version)} --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --non-interactive`,
|
|
263
|
+
};
|
|
264
|
+
case "maven-central":
|
|
265
|
+
return {
|
|
266
|
+
...shared,
|
|
267
|
+
secrets: [
|
|
268
|
+
"MAVEN_CENTRAL_USERNAME",
|
|
269
|
+
"MAVEN_CENTRAL_PASSWORD",
|
|
270
|
+
"MAVEN_GPG_PRIVATE_KEY",
|
|
271
|
+
"MAVEN_GPG_PASSPHRASE",
|
|
272
|
+
],
|
|
273
|
+
exists: `curl -fsS ${shellQuote(mavenSearch)} | node -e ${shellQuote("let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.exit(JSON.parse(s).response.numFound>0?0:1))")}`,
|
|
274
|
+
gate: '[ -n "$MAVEN_CENTRAL_USERNAME" ] && [ -n "$MAVEN_CENTRAL_PASSWORD" ] && [ -n "$MAVEN_GPG_PRIVATE_KEY" ] && [ -n "$MAVEN_GPG_PASSPHRASE" ]',
|
|
275
|
+
retractUrl: "https://central.sonatype.com/publishing/deployments",
|
|
276
|
+
};
|
|
277
|
+
case "rubygems":
|
|
278
|
+
return {
|
|
279
|
+
...shared,
|
|
280
|
+
secrets: ["RUBYGEMS_API_KEY"],
|
|
281
|
+
exists: `curl -fsS ${shellQuote(`https://rubygems.org/api/v1/versions/${encodeURIComponent(entry.name)}.json`)} | node -e ${shellQuote(`let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.exit(JSON.parse(s).some(v=>v.number===${JSON.stringify(version)})?0:1))`)}`,
|
|
282
|
+
gate: '[ -n "$RUBYGEMS_API_KEY" ]',
|
|
283
|
+
retract: `GEM_HOST_API_KEY="$RUBYGEMS_API_KEY" gem yank ${shellQuote(entry.name)} -v ${shellQuote(version)}`,
|
|
284
|
+
};
|
|
285
|
+
case "packagist":
|
|
286
|
+
return {
|
|
287
|
+
...shared,
|
|
288
|
+
secrets: [],
|
|
289
|
+
gate: "true",
|
|
290
|
+
retractUrl: "https://packagist.org/packages",
|
|
291
|
+
};
|
|
292
|
+
case "go-module":
|
|
293
|
+
return {
|
|
294
|
+
...shared,
|
|
295
|
+
secrets: [],
|
|
296
|
+
gate: "true",
|
|
297
|
+
retractUrl: "https://go.dev/ref/mod#go-mod-file-retract-directive",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function nativeRegistryRows(project) {
|
|
302
|
+
return activeConfigs(project).map((entry) => row(project, entry));
|
|
303
|
+
}
|
|
304
|
+
const checkout = (releaseSha) => ({
|
|
305
|
+
uses: "actions/checkout@v7",
|
|
306
|
+
with: { ref: releaseSha },
|
|
307
|
+
});
|
|
308
|
+
const download = (artifactDir) => ({
|
|
309
|
+
uses: "actions/download-artifact@v8",
|
|
310
|
+
with: { name: "release-assets", path: artifactDir },
|
|
311
|
+
});
|
|
312
|
+
/** GitHub Actions jobs use the upstream language tool and package client directly. */
|
|
313
|
+
export function nativePublishJobs(project, artifactDir, releaseSha) {
|
|
314
|
+
return Object.fromEntries(activeConfigs(project).flatMap((entry) => {
|
|
315
|
+
if (entry.id === "packagist" || entry.id === "go-module")
|
|
316
|
+
return [];
|
|
317
|
+
const path = entry.path;
|
|
318
|
+
const base = {
|
|
319
|
+
needs: ["gate", "package"],
|
|
320
|
+
if: `needs.gate.outputs.${entry.id.replace(/-/g, "_")} == 'true'`,
|
|
321
|
+
"runs-on": "ubuntu-latest",
|
|
322
|
+
permissions: { contents: "read", "id-token": "write" },
|
|
323
|
+
};
|
|
324
|
+
let job;
|
|
325
|
+
switch (entry.id) {
|
|
326
|
+
case "cargo":
|
|
327
|
+
job = {
|
|
328
|
+
...base,
|
|
329
|
+
env: { CARGO_REGISTRY_TOKEN: "${{ secrets.CARGO_REGISTRY_TOKEN }}" },
|
|
330
|
+
steps: [
|
|
331
|
+
checkout(releaseSha),
|
|
332
|
+
{ run: "cargo --version" },
|
|
333
|
+
{
|
|
334
|
+
run: `cd ${shellQuote(path)} && ${versionAssert(entry, project.identity.version)} && cargo publish --manifest-path ${shellQuote(entry.identity)} --token "$CARGO_REGISTRY_TOKEN"`,
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
break;
|
|
339
|
+
case "nuget":
|
|
340
|
+
job = {
|
|
341
|
+
...base,
|
|
342
|
+
env: { NUGET_API_KEY: "${{ secrets.NUGET_API_KEY }}" },
|
|
343
|
+
steps: [
|
|
344
|
+
{ uses: "actions/setup-dotnet@v5", with: { "dotnet-version": "8.0.x" } },
|
|
345
|
+
{ run: "dotnet --info" },
|
|
346
|
+
download(artifactDir),
|
|
347
|
+
{
|
|
348
|
+
run: `find ${shellQuote(`${artifactDir}/native/nuget`)} -maxdepth 1 -name '*.nupkg' -print0 | xargs -0 -r -n1 sh -c 'dotnet nuget push "$0" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate'`,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
break;
|
|
353
|
+
case "maven-central":
|
|
354
|
+
job = {
|
|
355
|
+
...base,
|
|
356
|
+
env: {
|
|
357
|
+
MAVEN_CENTRAL_USERNAME: "${{ secrets.MAVEN_CENTRAL_USERNAME }}",
|
|
358
|
+
MAVEN_CENTRAL_PASSWORD: "${{ secrets.MAVEN_CENTRAL_PASSWORD }}",
|
|
359
|
+
MAVEN_GPG_PRIVATE_KEY: "${{ secrets.MAVEN_GPG_PRIVATE_KEY }}",
|
|
360
|
+
MAVEN_GPG_PASSPHRASE: "${{ secrets.MAVEN_GPG_PASSPHRASE }}",
|
|
361
|
+
},
|
|
362
|
+
steps: [
|
|
363
|
+
checkout(releaseSha),
|
|
364
|
+
{
|
|
365
|
+
uses: "actions/setup-java@v5",
|
|
366
|
+
with: {
|
|
367
|
+
distribution: "temurin",
|
|
368
|
+
"java-version": "21",
|
|
369
|
+
cache: "maven",
|
|
370
|
+
"server-id": "central",
|
|
371
|
+
"server-username": "MAVEN_CENTRAL_USERNAME",
|
|
372
|
+
"server-password": "MAVEN_CENTRAL_PASSWORD",
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
{ run: 'echo "$MAVEN_GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import' },
|
|
376
|
+
{
|
|
377
|
+
run: `cd ${shellQuote(path)} && ${versionAssert(entry, project.identity.version)} && mvn --batch-mode --file ${shellQuote(entry.identity)} -DskipTests -Dgpg.passphrase="$MAVEN_GPG_PASSPHRASE" deploy`,
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
break;
|
|
382
|
+
case "rubygems":
|
|
383
|
+
job = {
|
|
384
|
+
...base,
|
|
385
|
+
env: { GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_API_KEY }}" },
|
|
386
|
+
steps: [
|
|
387
|
+
{ run: "gem --version" },
|
|
388
|
+
download(artifactDir),
|
|
389
|
+
{
|
|
390
|
+
run: `gem push ${shellQuote(`${artifactDir}/native/rubygems/${entry.name}-${project.identity.version}.gem`)}`,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
const steps = job.steps;
|
|
397
|
+
steps.unshift({ uses: "actions/setup-node@v7", with: { "node-version": "24" } });
|
|
398
|
+
const publish = steps.at(-1);
|
|
399
|
+
const exists = row(project, entry).exists;
|
|
400
|
+
if (publish && exists)
|
|
401
|
+
publish.run = `if ${exists}; then echo "${entry.id}: version already published"; else ${publish.run}; fi`;
|
|
402
|
+
return [[`publish-${entry.id}`, job]];
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
export function nativeInstallLines(project) {
|
|
406
|
+
return activeConfigs(project).map((entry) => ({
|
|
407
|
+
cargo: `\`cargo install ${entry.name}\``,
|
|
408
|
+
nuget: `\`dotnet add package ${entry.name}\``,
|
|
409
|
+
"maven-central": `add \`${entry.name}\` as a Maven dependency`,
|
|
410
|
+
rubygems: `\`gem install ${entry.name}\``,
|
|
411
|
+
packagist: `\`composer require ${entry.name}\``,
|
|
412
|
+
"go-module": `\`go get ${entry.name}@${project.identity.version ? `v${project.identity.version}` : "latest"}\``,
|
|
413
|
+
})[entry.id]);
|
|
414
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|