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.
- package/README.md +166 -0
- package/dist/bindings/index.d.ts +23 -0
- package/dist/bindings/index.js +29 -0
- package/dist/bindings/python.d.ts +33 -0
- package/dist/bindings/python.js +805 -0
- package/dist/bindings/python.test.d.ts +1 -0
- package/dist/bindings/python.test.js +316 -0
- package/dist/bindings/typescript.d.ts +39 -0
- package/dist/bindings/typescript.js +758 -0
- package/dist/bindings/typescript.test.d.ts +1 -0
- package/dist/bindings/typescript.test.js +264 -0
- package/dist/commands.d.ts +188 -0
- package/dist/commands.js +662 -0
- package/dist/commands.test.d.ts +1 -0
- package/dist/commands.test.js +215 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +124 -0
- package/dist/hosts/browser.d.ts +10 -0
- package/dist/hosts/browser.js +94 -0
- package/dist/hosts/github.d.ts +66 -0
- package/dist/hosts/github.js +247 -0
- package/dist/hosts/openclaw.d.ts +4 -0
- package/dist/hosts/openclaw.js +94 -0
- package/dist/hosts/template.d.ts +15 -0
- package/dist/hosts/template.js +142 -0
- package/dist/hosts/web.d.ts +4 -0
- package/dist/hosts/web.js +89 -0
- package/dist/identity/name.d.ts +20 -0
- package/dist/identity/name.js +33 -0
- package/dist/identity/name.test.d.ts +1 -0
- package/dist/identity/name.test.js +37 -0
- package/dist/identity/read.d.ts +9 -0
- package/dist/identity/read.js +106 -0
- package/dist/identity/read.test.d.ts +1 -0
- package/dist/identity/read.test.js +76 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/introspect/index.d.ts +15 -0
- package/dist/introspect/index.js +63 -0
- package/dist/model.d.ts +236 -0
- package/dist/model.js +214 -0
- package/dist/ops.d.ts +255 -0
- package/dist/ops.js +297 -0
- package/dist/project/apply.d.ts +35 -0
- package/dist/project/apply.js +360 -0
- package/dist/project/apply.test.d.ts +1 -0
- package/dist/project/apply.test.js +285 -0
- package/dist/project/gate.d.ts +129 -0
- package/dist/project/gate.js +597 -0
- package/dist/project/history.d.ts +62 -0
- package/dist/project/history.js +41 -0
- package/dist/project/load.d.ts +21 -0
- package/dist/project/load.js +82 -0
- package/dist/project/lock.d.ts +28 -0
- package/dist/project/lock.js +42 -0
- package/dist/project/plan.d.ts +5 -0
- package/dist/project/plan.js +96 -0
- package/dist/report/coverage.d.ts +22 -0
- package/dist/report/coverage.js +72 -0
- package/dist/report/coverage.test.d.ts +1 -0
- package/dist/report/coverage.test.js +93 -0
- package/dist/surfaces/agent-plugins.d.ts +10 -0
- package/dist/surfaces/agent-plugins.js +36 -0
- package/dist/surfaces/agents.d.ts +35 -0
- package/dist/surfaces/agents.js +609 -0
- package/dist/surfaces/agents.template.d.ts +8 -0
- package/dist/surfaces/agents.template.js +21 -0
- package/dist/surfaces/agents.test.d.ts +1 -0
- package/dist/surfaces/agents.test.js +181 -0
- package/dist/surfaces/browser-extension.d.ts +101 -0
- package/dist/surfaces/browser-extension.js +1034 -0
- package/dist/surfaces/browser-extension.test.d.ts +1 -0
- package/dist/surfaces/browser-extension.test.js +145 -0
- package/dist/surfaces/claude.d.ts +2 -0
- package/dist/surfaces/claude.js +81 -0
- package/dist/surfaces/codex.d.ts +13 -0
- package/dist/surfaces/codex.js +101 -0
- package/dist/surfaces/codex.test.d.ts +1 -0
- package/dist/surfaces/codex.test.js +67 -0
- package/dist/surfaces/cursor.d.ts +2 -0
- package/dist/surfaces/cursor.js +50 -0
- package/dist/surfaces/distribution.test.d.ts +1 -0
- package/dist/surfaces/distribution.test.js +146 -0
- package/dist/surfaces/dsh.d.ts +25 -0
- package/dist/surfaces/dsh.js +147 -0
- package/dist/surfaces/dsh.test.d.ts +1 -0
- package/dist/surfaces/dsh.test.js +100 -0
- package/dist/surfaces/external.d.ts +9 -0
- package/dist/surfaces/external.js +4 -0
- package/dist/surfaces/gemini.d.ts +29 -0
- package/dist/surfaces/gemini.js +68 -0
- package/dist/surfaces/gemini.test.d.ts +1 -0
- package/dist/surfaces/gemini.test.js +74 -0
- package/dist/surfaces/hermes-native.d.ts +53 -0
- package/dist/surfaces/hermes-native.js +507 -0
- package/dist/surfaces/hermes-native.test.d.ts +1 -0
- package/dist/surfaces/hermes-native.test.js +156 -0
- package/dist/surfaces/kernel.d.ts +5 -0
- package/dist/surfaces/kernel.js +63 -0
- package/dist/surfaces/mcp-registry.d.ts +19 -0
- package/dist/surfaces/mcp-registry.js +160 -0
- package/dist/surfaces/mcp-registry.test.d.ts +1 -0
- package/dist/surfaces/mcp-registry.test.js +79 -0
- package/dist/surfaces/mcpb.d.ts +11 -0
- package/dist/surfaces/mcpb.js +118 -0
- package/dist/surfaces/mcpb.test.d.ts +1 -0
- package/dist/surfaces/mcpb.test.js +104 -0
- package/dist/surfaces/npm.d.ts +6 -0
- package/dist/surfaces/npm.js +72 -0
- package/dist/surfaces/openclaw-native.d.ts +93 -0
- package/dist/surfaces/openclaw-native.js +956 -0
- package/dist/surfaces/openclaw-native.test.d.ts +1 -0
- package/dist/surfaces/openclaw-native.test.js +246 -0
- package/dist/surfaces/pypi.d.ts +5 -0
- package/dist/surfaces/pypi.js +82 -0
- package/dist/surfaces/readme.d.ts +7 -0
- package/dist/surfaces/readme.js +181 -0
- package/dist/surfaces/registry.d.ts +6 -0
- package/dist/surfaces/registry.js +73 -0
- package/dist/surfaces/shared.d.ts +49 -0
- package/dist/surfaces/shared.js +112 -0
- package/dist/surfaces/skill.d.ts +7 -0
- package/dist/surfaces/skill.js +118 -0
- package/dist/surfaces/skill.test.d.ts +1 -0
- package/dist/surfaces/skill.test.js +87 -0
- package/dist/surfaces/web.d.ts +97 -0
- package/dist/surfaces/web.js +1285 -0
- package/dist/surfaces/web.test.d.ts +1 -0
- package/dist/surfaces/web.test.js +255 -0
- package/dist/surfaces/workflows.d.ts +2 -0
- package/dist/surfaces/workflows.js +809 -0
- package/dist/surfaces/workflows.test.d.ts +1 -0
- package/dist/surfaces/workflows.test.js +563 -0
- package/dist/toolfactory/cli.d.ts +2 -0
- package/dist/toolfactory/cli.js +65 -0
- package/dist/toolfactory/config.d.ts +3 -0
- package/dist/toolfactory/config.js +27 -0
- package/dist/toolfactory/mcp.d.ts +37 -0
- package/dist/toolfactory/mcp.js +269 -0
- package/dist/toolfactory/types.d.ts +31 -0
- package/dist/toolfactory/types.js +10 -0
- package/dist/toolfactory/web.d.ts +4 -0
- package/dist/toolfactory/web.js +40 -0
- package/dist/validate/agent-plugins.d.ts +5 -0
- package/dist/validate/agent-plugins.js +46 -0
- package/package.json +68 -0
- package/schemas/agent-plugins/mcp.schema.json +120 -0
- package/schemas/agent-plugins/plugin.schema.json +65 -0
- package/src/bindings/index.ts +47 -0
- package/src/bindings/python.test.ts +374 -0
- package/src/bindings/python.ts +862 -0
- package/src/bindings/typescript.test.ts +329 -0
- package/src/bindings/typescript.ts +818 -0
- package/src/commands.test.ts +253 -0
- package/src/commands.ts +886 -0
- package/src/e2e.test.ts +169 -0
- package/src/hosts/browser.ts +106 -0
- package/src/hosts/github.ts +321 -0
- package/src/hosts/openclaw.ts +110 -0
- package/src/hosts/template.ts +149 -0
- package/src/hosts/web.ts +111 -0
- package/src/identity/name.test.ts +43 -0
- package/src/identity/name.ts +39 -0
- package/src/identity/read.test.ts +92 -0
- package/src/identity/read.ts +119 -0
- package/src/index.ts +11 -0
- package/src/introspect/index.ts +72 -0
- package/src/model.ts +386 -0
- package/src/ops.ts +333 -0
- package/src/project/apply.test.ts +348 -0
- package/src/project/apply.ts +417 -0
- package/src/project/gate.ts +717 -0
- package/src/project/history.ts +46 -0
- package/src/project/load.ts +104 -0
- package/src/project/lock.ts +54 -0
- package/src/project/plan.ts +101 -0
- package/src/report/coverage.test.ts +108 -0
- package/src/report/coverage.ts +113 -0
- package/src/surfaces/agent-plugins.ts +47 -0
- package/src/surfaces/agents.template.ts +32 -0
- package/src/surfaces/agents.test.ts +206 -0
- package/src/surfaces/agents.ts +729 -0
- package/src/surfaces/browser-extension.test.ts +175 -0
- package/src/surfaces/browser-extension.ts +1095 -0
- package/src/surfaces/claude.ts +99 -0
- package/src/surfaces/codex.test.ts +71 -0
- package/src/surfaces/codex.ts +105 -0
- package/src/surfaces/cursor.ts +62 -0
- package/src/surfaces/distribution.test.ts +166 -0
- package/src/surfaces/dsh.test.ts +110 -0
- package/src/surfaces/dsh.ts +159 -0
- package/src/surfaces/external.ts +13 -0
- package/src/surfaces/gemini.test.ts +80 -0
- package/src/surfaces/gemini.ts +84 -0
- package/src/surfaces/hermes-native.test.ts +179 -0
- package/src/surfaces/hermes-native.ts +584 -0
- package/src/surfaces/kernel.ts +68 -0
- package/src/surfaces/mcp-registry.test.ts +99 -0
- package/src/surfaces/mcp-registry.ts +190 -0
- package/src/surfaces/mcpb.test.ts +112 -0
- package/src/surfaces/mcpb.ts +130 -0
- package/src/surfaces/npm.ts +78 -0
- package/src/surfaces/openclaw-native.test.ts +283 -0
- package/src/surfaces/openclaw-native.ts +1070 -0
- package/src/surfaces/pypi.ts +84 -0
- package/src/surfaces/readme.ts +218 -0
- package/src/surfaces/registry.ts +79 -0
- package/src/surfaces/shared.ts +140 -0
- package/src/surfaces/skill.test.ts +103 -0
- package/src/surfaces/skill.ts +136 -0
- package/src/surfaces/web.test.ts +303 -0
- package/src/surfaces/web.ts +1310 -0
- package/src/surfaces/workflows.test.ts +677 -0
- package/src/surfaces/workflows.ts +936 -0
- package/src/toolfactory/cli.ts +74 -0
- package/src/toolfactory/config.ts +33 -0
- package/src/toolfactory/mcp.ts +311 -0
- package/src/toolfactory/types.ts +52 -0
- package/src/toolfactory/web.ts +45 -0
- package/src/validate/agent-plugins.ts +53 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-CCXhQKK3.css +2 -0
- package/web/dist/assets/index-D1PwZ3qS.js +10 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate and the release assets, as data.
|
|
3
|
+
*
|
|
4
|
+
* The gate is a command; CI is its projection. One list of steps is rendered three ways — the
|
|
5
|
+
* `test` job of `ci.yml`, the `gate`/`package` jobs of `release.yml`, and a shell script a local
|
|
6
|
+
* `toolfactory gate` / `toolfactory package` spawns — so "what green means" has exactly one
|
|
7
|
+
* definition whether or not the project ever sees GitHub.
|
|
8
|
+
*
|
|
9
|
+
* A step here is a command and nothing else. GitHub Actions `uses:` steps are runner provisioning
|
|
10
|
+
* (checkout, toolchains, caches) and stay in `src/surfaces/workflows.ts`, which wraps this list.
|
|
11
|
+
*/
|
|
12
|
+
import { githubSlug } from "../hosts/github.js";
|
|
13
|
+
import { githubOwner, projectName } from "../identity/name.js";
|
|
14
|
+
import { isInstructionOnly, type PackageManager, type Project, type SurfaceId } from "../model.js";
|
|
15
|
+
import {
|
|
16
|
+
HOST_DIR as BROWSER_HOST_DIR,
|
|
17
|
+
sourcesZipName,
|
|
18
|
+
zipName,
|
|
19
|
+
} from "../surfaces/browser-extension.js";
|
|
20
|
+
import { HOST_DIR as DSH_HOST_DIR } from "../surfaces/dsh.js";
|
|
21
|
+
import { MANIFEST_PATH as MCPB_MANIFEST_PATH, MCPB_PIN } from "../surfaces/mcpb.js";
|
|
22
|
+
import { HOST_DIR as OPENCLAW_HOST_DIR } from "../surfaces/openclaw-native.js";
|
|
23
|
+
import {
|
|
24
|
+
configProperties,
|
|
25
|
+
envName,
|
|
26
|
+
has,
|
|
27
|
+
isSensitive,
|
|
28
|
+
npmName,
|
|
29
|
+
pypiName,
|
|
30
|
+
} from "../surfaces/shared.js";
|
|
31
|
+
|
|
32
|
+
export interface GateStep {
|
|
33
|
+
/** Step label: the CI step name, and the banner the shell script echoes. */
|
|
34
|
+
name: string;
|
|
35
|
+
run: string;
|
|
36
|
+
/**
|
|
37
|
+
* `"ci"` — runner provisioning no checkout may perform on the author's machine (a global or
|
|
38
|
+
* system-wide install); the local runner skips it. `"node24"` — needs Node >= 24, which the
|
|
39
|
+
* `openclaw` CLI does; the matrix guard in `ci.yml`.
|
|
40
|
+
*/
|
|
41
|
+
when?: "ci" | "node24";
|
|
42
|
+
env?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Where `toolfactory package` and the release `package` job leave every asset. */
|
|
46
|
+
export const RELEASE_DIR = "dist/release";
|
|
47
|
+
|
|
48
|
+
/** The web app's build, shared by the release tarball and the Pages job (which sets `PAGES_BASE`). */
|
|
49
|
+
export const WEB_BUILD = "npm -C web ci && npm -C web run build";
|
|
50
|
+
|
|
51
|
+
/** The commands each supported package manager spells; `package.json` `packageManager` picks the row. */
|
|
52
|
+
export const PACKAGE_MANAGER_COMMANDS: Record<
|
|
53
|
+
PackageManager,
|
|
54
|
+
{ install: string; run: (script: string) => string; test: string }
|
|
55
|
+
> = {
|
|
56
|
+
npm: {
|
|
57
|
+
install: "npm ci",
|
|
58
|
+
run: (script) => `npm run --if-present ${script}`,
|
|
59
|
+
test: "npm test",
|
|
60
|
+
},
|
|
61
|
+
pnpm: {
|
|
62
|
+
install: "pnpm install --frozen-lockfile",
|
|
63
|
+
run: (script) => `pnpm run --if-present ${script}`,
|
|
64
|
+
test: "pnpm test",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** How the project invokes toolfactory: the devDependency in a TypeScript project, a pinned fetch otherwise. */
|
|
69
|
+
export function toolfactoryCli(project: Project): string {
|
|
70
|
+
if (isInstructionOnly(project.tool)) return `npx --yes toolfactory@${project.toolfactoryVersion}`;
|
|
71
|
+
return project.tool.binding === "typescript"
|
|
72
|
+
? "npx toolfactory"
|
|
73
|
+
: `npx --yes toolfactory@${project.toolfactoryVersion}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function commands(project: Project) {
|
|
77
|
+
return PACKAGE_MANAGER_COMMANDS[project.packageManager ?? "npm"];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Dependencies and, for TypeScript, the build — everything both the gate and the packaging run
|
|
82
|
+
* need before any command that imports the project's own code.
|
|
83
|
+
*/
|
|
84
|
+
/**
|
|
85
|
+
* Output files (§2.2 S4) are build products, not tracked: a fresh checkout has none, and the
|
|
86
|
+
* validators and the web build read them. On a tree `check` has just proved current, `build`
|
|
87
|
+
* writes exactly those.
|
|
88
|
+
*/
|
|
89
|
+
export function outputsStep(project: Project): GateStep {
|
|
90
|
+
return { name: "toolfactory build (output files)", run: `${toolfactoryCli(project)} build` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function bootstrapSteps(project: Project): GateStep[] {
|
|
94
|
+
if (isInstructionOnly(project.tool)) return [];
|
|
95
|
+
const pm = commands(project);
|
|
96
|
+
return project.tool.binding === "typescript"
|
|
97
|
+
? [
|
|
98
|
+
{ name: "install", run: pm.install, when: "ci" },
|
|
99
|
+
{ name: "build", run: pm.run("build") },
|
|
100
|
+
]
|
|
101
|
+
: [{ name: "install", run: "uv sync", when: "ci" }];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The gate: bootstrap, the drift check, the validator CLIs the selected surfaces need on PATH,
|
|
106
|
+
* `toolfactory validate` (every surface's own upstream validator), the author's `check` and
|
|
107
|
+
* tests, and the credential-free OpenClaw turn. Nothing is transcribed from a surface — the
|
|
108
|
+
* commands a surface owns are reached through `toolfactory validate`, so this list cannot drift
|
|
109
|
+
* away from what `validate` actually runs.
|
|
110
|
+
*/
|
|
111
|
+
export function gateSteps(project: Project): GateStep[] {
|
|
112
|
+
if (isInstructionOnly(project.tool)) {
|
|
113
|
+
const cli = toolfactoryCli(project);
|
|
114
|
+
return [
|
|
115
|
+
{ name: "toolfactory check", run: `${cli} check` },
|
|
116
|
+
outputsStep(project),
|
|
117
|
+
{ name: "toolfactory validate", run: `${cli} validate` },
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
const typescript = project.tool.binding === "typescript";
|
|
121
|
+
const pm = commands(project);
|
|
122
|
+
const cli = toolfactoryCli(project);
|
|
123
|
+
const openclaw = has(project, "openclaw-native");
|
|
124
|
+
const steps: GateStep[] = [
|
|
125
|
+
...bootstrapSteps(project),
|
|
126
|
+
{ name: "toolfactory check", run: `${cli} check` },
|
|
127
|
+
outputsStep(project),
|
|
128
|
+
];
|
|
129
|
+
if (has(project, "claude")) {
|
|
130
|
+
steps.push({
|
|
131
|
+
name: "Install Claude Code CLI",
|
|
132
|
+
run: "npm i -g @anthropic-ai/claude-code",
|
|
133
|
+
when: "ci",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (has(project, "web") || has(project, "browser-extension")) {
|
|
137
|
+
// The web smoke and the browser extension's own Playwright smoke both drive Chromium;
|
|
138
|
+
// their own npm installs fetch the browser, the runner needs its system libraries.
|
|
139
|
+
steps.push({
|
|
140
|
+
name: "Install Chromium dependencies",
|
|
141
|
+
run: "npx --yes playwright install-deps chromium",
|
|
142
|
+
when: "ci",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
steps.push({
|
|
146
|
+
name: "toolfactory validate",
|
|
147
|
+
run: `${cli} validate`,
|
|
148
|
+
// openclaw-native's validate() shells to the openclaw CLI, which needs Node >=24.
|
|
149
|
+
...(openclaw ? { when: "node24" as const } : {}),
|
|
150
|
+
});
|
|
151
|
+
if (typescript) steps.push({ name: "author checks", run: pm.run("check") });
|
|
152
|
+
steps.push({
|
|
153
|
+
name: "author tests",
|
|
154
|
+
run: typescript ? pm.test : "uv run --with pytest pytest -q",
|
|
155
|
+
});
|
|
156
|
+
if (openclaw) {
|
|
157
|
+
// T3, credential-free: one real OpenClaw agent turn against a scripted model. The suite only
|
|
158
|
+
// exists when `tool.json` `tests.examples` names an operation the plugin carries, which is
|
|
159
|
+
// exactly what npm's own `--if-present` asks.
|
|
160
|
+
steps.push({
|
|
161
|
+
name: "openclaw end-to-end (scripted model, no LLM key)",
|
|
162
|
+
run: `npm --prefix ${OPENCLAW_HOST_DIR} run --if-present test:e2e`,
|
|
163
|
+
when: "node24",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return steps;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* §2.2: one version, in the identity file, projected everywhere by `build`. A release therefore
|
|
171
|
+
* asserts the tag against it and never writes it — rewriting a SHA-locked file from the tag would
|
|
172
|
+
* be a change `build` did not make and `check` would fail on the next run.
|
|
173
|
+
*/
|
|
174
|
+
export function tagVersionAssert(project: Project): GateStep | undefined {
|
|
175
|
+
const identity = project.tool.identity;
|
|
176
|
+
const read = identity.endsWith(".json")
|
|
177
|
+
? `node -p "require('./${identity}').version"`
|
|
178
|
+
: identity.endsWith(".toml")
|
|
179
|
+
? `python3 -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('${identity}').read_text())['project']['version'])"`
|
|
180
|
+
: undefined;
|
|
181
|
+
if (!read) return undefined;
|
|
182
|
+
return {
|
|
183
|
+
// `RELEASE_TAG`, not `GITHUB_REF_NAME`: `release.yml` also runs on `workflow_dispatch` with a
|
|
184
|
+
// `tag` input — the only re-run that reads current secrets — and then the ref is a branch.
|
|
185
|
+
name: `tag matches ${identity}`,
|
|
186
|
+
run: `test "v$(${read})" = "$RELEASE_TAG"`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The bundle paths a plugin zip carries, in the order a reader wants them: identity, then payload. */
|
|
191
|
+
function bundlePaths(project: Project): string[] {
|
|
192
|
+
const paths: string[] = [];
|
|
193
|
+
if (has(project, "agent-plugins")) paths.push(project.tool.identity, "mcp.json");
|
|
194
|
+
if (has(project, "skill")) paths.push("skills");
|
|
195
|
+
if (has(project, "claude")) paths.push(".claude-plugin");
|
|
196
|
+
if (has(project, "codex")) paths.push(".codex-plugin");
|
|
197
|
+
if (has(project, "cursor")) paths.push(".cursor-plugin");
|
|
198
|
+
return paths;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** License notices stay with a plugin bundle when the author carries them at the repository root. */
|
|
202
|
+
const LEGAL_BUNDLE_PATHS = [
|
|
203
|
+
"LICENSE",
|
|
204
|
+
"LICENSE.md",
|
|
205
|
+
"LICENSE.txt",
|
|
206
|
+
"NOTICE",
|
|
207
|
+
"NOTICE.md",
|
|
208
|
+
"NOTICE.txt",
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The release assets, into `dist/release/`: the same list locally (`toolfactory package`) and in
|
|
213
|
+
* the release workflow's `package` job, which uploads the directory as one artifact. Publishing
|
|
214
|
+
* to a registry stays a CI concern; producing the artifacts never is.
|
|
215
|
+
*/
|
|
216
|
+
export function packageSteps(project: Project): GateStep[] {
|
|
217
|
+
const cli = toolfactoryCli(project);
|
|
218
|
+
const name = project.identity.name;
|
|
219
|
+
const steps: GateStep[] = [
|
|
220
|
+
{ name: "release directory", run: `rm -rf ${RELEASE_DIR} && mkdir -p ${RELEASE_DIR}` },
|
|
221
|
+
...bootstrapSteps(project),
|
|
222
|
+
outputsStep(project),
|
|
223
|
+
];
|
|
224
|
+
if (has(project, "web")) {
|
|
225
|
+
steps.push({
|
|
226
|
+
name: "web build",
|
|
227
|
+
// Build before packing: npm and MCPB both carry this same root-relative page.
|
|
228
|
+
env: { PAGES_BASE: "/" },
|
|
229
|
+
run: WEB_BUILD,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (has(project, "npm")) {
|
|
233
|
+
steps.push({ name: "npm tarball", run: `npm pack --pack-destination ${RELEASE_DIR}` });
|
|
234
|
+
}
|
|
235
|
+
if (has(project, "mcpb")) {
|
|
236
|
+
// A bundle root is the published package with its production dependencies installed into
|
|
237
|
+
// it, so it is built from the tarball `npm pack` just wrote rather than from the checkout:
|
|
238
|
+
// whatever ships to npm is exactly what ships to Claude Desktop. `--ignore-scripts` because
|
|
239
|
+
// nothing in a bundle root may run a build; the tarball already carries `dist/`.
|
|
240
|
+
const stage = "dist/mcpb";
|
|
241
|
+
const tarball = npmTarball(project);
|
|
242
|
+
steps.push({
|
|
243
|
+
name: "MCPB bundle",
|
|
244
|
+
run: [
|
|
245
|
+
`rm -rf ${stage} && mkdir -p ${stage}`,
|
|
246
|
+
`tar -xzf ${RELEASE_DIR}/${tarball} -C ${stage} --strip-components=1`,
|
|
247
|
+
`npm --prefix ${stage} install --omit=dev --ignore-scripts`,
|
|
248
|
+
`cp ${MCPB_MANIFEST_PATH} ${stage}/manifest.json`,
|
|
249
|
+
`npx -y @anthropic-ai/mcpb@${MCPB_PIN} pack ${stage} ${RELEASE_DIR}/${name}.mcpb`,
|
|
250
|
+
].join(" && "),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (has(project, "pypi")) {
|
|
254
|
+
steps.push({ name: "python distributions", run: `uv build --out-dir ${RELEASE_DIR}` });
|
|
255
|
+
}
|
|
256
|
+
if (has(project, "openclaw-native")) {
|
|
257
|
+
// ClawHub's reusable workflow has no build step of its own, so the tarball it publishes has
|
|
258
|
+
// to arrive already built (`package_artifact_name`).
|
|
259
|
+
steps.push({
|
|
260
|
+
name: "OpenClaw plugin tarball",
|
|
261
|
+
run: [
|
|
262
|
+
`npm --prefix ${OPENCLAW_HOST_DIR} install`,
|
|
263
|
+
`npm --prefix ${OPENCLAW_HOST_DIR} run build`,
|
|
264
|
+
`npm pack ./${OPENCLAW_HOST_DIR} --pack-destination ${RELEASE_DIR}`,
|
|
265
|
+
].join(" && "),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (has(project, "dsh")) {
|
|
269
|
+
// Two files and no code: nothing to build, and `dsh plugin add` takes a tarball directly.
|
|
270
|
+
steps.push({
|
|
271
|
+
name: "DSH bundle tarball",
|
|
272
|
+
run: `npm pack ./${DSH_HOST_DIR} --pack-destination ${RELEASE_DIR}`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const bundle = bundlePaths(project);
|
|
276
|
+
if (bundle.length) {
|
|
277
|
+
steps.push({
|
|
278
|
+
name: "plugin bundle",
|
|
279
|
+
run: `set -- ${bundle.join(" ")}; for path in ${LEGAL_BUNDLE_PATHS.join(" ")}; do if [ -f "$path" ]; then set -- "$@" "$path"; fi; done; zip -qr ${RELEASE_DIR}/${name}-plugin.zip "$@"`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (has(project, "web")) {
|
|
283
|
+
steps.push({
|
|
284
|
+
name: "web tarball",
|
|
285
|
+
run: `tar -czf ${RELEASE_DIR}/${name}-web.tar.gz -C web/dist .`,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (has(project, "browser-extension")) {
|
|
289
|
+
// `wxt zip` writes into hosts/browser/.output/ under exactly the names `zipName`/
|
|
290
|
+
// `sourcesZipName` predict (verified live), so no rename is needed, only a copy.
|
|
291
|
+
const zipTargets: Array<"chrome" | "firefox" | "edge"> = ["chrome", "firefox", "edge"];
|
|
292
|
+
const outDir = `${BROWSER_HOST_DIR}/.output`;
|
|
293
|
+
steps.push({
|
|
294
|
+
name: "browser extension zips",
|
|
295
|
+
run: [
|
|
296
|
+
`npm --prefix ${BROWSER_HOST_DIR} install`,
|
|
297
|
+
// `npm exec` never changes the working directory (unlike `npm run`), so `wxt` needs its
|
|
298
|
+
// root passed explicitly — verified live: without it, `wxt zip` looks for `./entrypoints`
|
|
299
|
+
// relative to the repo root instead of hosts/browser.
|
|
300
|
+
...zipTargets.map(
|
|
301
|
+
(browser) =>
|
|
302
|
+
`npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt zip ${BROWSER_HOST_DIR} -b ${browser}`,
|
|
303
|
+
),
|
|
304
|
+
...zipTargets.map((browser) => `cp ${outDir}/${zipName(project, browser)} ${RELEASE_DIR}/`),
|
|
305
|
+
`cp ${outDir}/${sourcesZipName(project)} ${RELEASE_DIR}/`,
|
|
306
|
+
].join(" && "),
|
|
307
|
+
});
|
|
308
|
+
steps.push({
|
|
309
|
+
// Self-hosted Firefox distribution (§7): a Mozilla-signed, install-anywhere xpi. Opt-in on
|
|
310
|
+
// the JWT pair alone — no store listing is created (`--channel=unlisted`) — so a project
|
|
311
|
+
// with the surface selected but no Firefox credentials configured yet still packages clean.
|
|
312
|
+
name: "Firefox signed xpi (only when FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET are set)",
|
|
313
|
+
run: `if [ -n "$FIREFOX_JWT_ISSUER" ] && [ -n "$FIREFOX_JWT_SECRET" ]; then npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext sign --channel=unlisted --api-key="$FIREFOX_JWT_ISSUER" --api-secret="$FIREFOX_JWT_SECRET" --source-dir ${outDir}/firefox-mv2 --artifacts-dir ${RELEASE_DIR}; fi`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// COVERAGE.md is tracked; coverage.json is a build output that need not be, so recompute it.
|
|
317
|
+
steps.push({
|
|
318
|
+
name: "coverage report",
|
|
319
|
+
run: `cp COVERAGE.md ${RELEASE_DIR}/ && ${cli} coverage > ${RELEASE_DIR}/coverage.json`,
|
|
320
|
+
});
|
|
321
|
+
return steps;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** npm's filename for the root package, including its flattened scope. */
|
|
325
|
+
export function npmTarball(project: Project): string {
|
|
326
|
+
return `${npmName(project).replace(/^@/, "").replace("/", "-")}-${project.identity.version ?? "0.0.0"}.tgz`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The ClawHub tarball `npm pack ./hosts/openclaw` writes, by name, inside the release artifact. */
|
|
330
|
+
export function openclawTarball(project: Project): string {
|
|
331
|
+
const pkg = projectName.openclawPackage(project.identity.name);
|
|
332
|
+
return `${pkg}-${project.identity.version ?? "0.0.0"}.tgz`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* One row per (surface, registry): what publishing needs, what proves it, how to retract.
|
|
337
|
+
* DESIGN.md §1 (research3). `secrets status`/`check`, the release legs' presence gating and the
|
|
338
|
+
* unpublish step all read this table and nothing else.
|
|
339
|
+
*/
|
|
340
|
+
export interface Registry {
|
|
341
|
+
/** Stable id: the release job suffix, the `secrets status` row, the unpublish step name. */
|
|
342
|
+
id:
|
|
343
|
+
| "npm"
|
|
344
|
+
| "pypi"
|
|
345
|
+
| "mcp-registry"
|
|
346
|
+
| "oci"
|
|
347
|
+
| "clawhub-package"
|
|
348
|
+
| "clawhub-skill"
|
|
349
|
+
| "pages"
|
|
350
|
+
| "chrome"
|
|
351
|
+
| "firefox"
|
|
352
|
+
| "edge"
|
|
353
|
+
| "safari";
|
|
354
|
+
/** The surfaces whose selection publishes here; all must be selected. */
|
|
355
|
+
surfaces: SurfaceId[];
|
|
356
|
+
/** Environment names the publish leg consumes (repository scope); [] for OIDC/GITHUB_TOKEN legs. */
|
|
357
|
+
secrets: string[];
|
|
358
|
+
/** Environment names retraction consumes when they differ from `secrets`. */
|
|
359
|
+
retractSecrets?: string[];
|
|
360
|
+
/** Where a human mints the credential or does the one-time web step. */
|
|
361
|
+
url: string;
|
|
362
|
+
/** Where a human retracts by hand, when that differs from `url`; the notices point at it. */
|
|
363
|
+
retractUrl?: string;
|
|
364
|
+
/** Shell: exit 0 iff the credential in the environment is accepted (`secrets check`). Absent = no probe exists. */
|
|
365
|
+
probe?: string;
|
|
366
|
+
/** Shell: exit 0 iff the current version is published (anonymous; idempotency for publish and unpublish). */
|
|
367
|
+
exists?: string;
|
|
368
|
+
/**
|
|
369
|
+
* Shell: exit 0 iff this leg can publish at this tag — the release `gate` job's
|
|
370
|
+
* `outputs.<id>`, which every leg's job-level `if` reads. Absent = the leg is never gated.
|
|
371
|
+
*/
|
|
372
|
+
gate?: string;
|
|
373
|
+
/** Shell: the retraction, one upstream CLI; absent = manual. */
|
|
374
|
+
retract?: string;
|
|
375
|
+
/** With `--hard`: the destructive form. */
|
|
376
|
+
retractHard?: string;
|
|
377
|
+
/** A one-time human step with no API, gated by a repository variable the human sets when done. */
|
|
378
|
+
confirmVariable?: string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** ClawHub's CLI, pinned: no runner carries `clawhub` on PATH and every ClawHub row shells to it. */
|
|
382
|
+
export const CLAWHUB_PIN = "0.23.3";
|
|
383
|
+
|
|
384
|
+
/** The `mcp-publisher` binary: the publish leg and the MCP Registry retraction fetch the same one. */
|
|
385
|
+
export const MCP_PUBLISHER_FETCH =
|
|
386
|
+
'curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher';
|
|
387
|
+
|
|
388
|
+
const CLAWHUB = `npx -y clawhub@${CLAWHUB_PIN}`;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* `clawhub` reads its token from `~/.config/clawhub/config.json`, never from the environment
|
|
392
|
+
* (verified: `CLAWHUB_TOKEN` in the environment still yields "Not logged in"). A throwaway HOME
|
|
393
|
+
* makes `login --token` — which validates the token itself, exit 1 on a revoked one — safe to run
|
|
394
|
+
* on a developer's machine without replacing the login they already have.
|
|
395
|
+
*/
|
|
396
|
+
function clawhubAuthed(command: string): string {
|
|
397
|
+
return `d=$(mktemp -d) && HOME="$d" ${CLAWHUB} login --token "$CLAWHUB_TOKEN" --no-input >/dev/null && HOME="$d" ${CLAWHUB} ${command}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* ClawHub's anonymous rate limit presents as a not-found carrying "(reset in Ns)", so a plain
|
|
402
|
+
* failure cannot be read as "absent" — retry once past the window before believing it.
|
|
403
|
+
*/
|
|
404
|
+
function clawhubExists(command: string): string {
|
|
405
|
+
return `${CLAWHUB} ${command} >/dev/null 2>&1 || { ${CLAWHUB} ${command} 2>&1 | grep -q "reset in" && sleep 60 && ${CLAWHUB} ${command} >/dev/null 2>&1; }`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** npm names its registry credential as a config key, not a variable, so `env` is the only prefix that can set it. */
|
|
409
|
+
function npmAuthed(command: string): string {
|
|
410
|
+
return `env "npm_config_//registry.npmjs.org/:_authToken=$NPM_TOKEN" ${command}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Every secret name of a set must be non-empty for its leg to run. */
|
|
414
|
+
function allSet(names: string[]): string {
|
|
415
|
+
return names.map((name) => `[ -n "$${name}" ]`).join(" && ");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const CHROME_SECRETS = [
|
|
419
|
+
"CHROME_EXTENSION_ID",
|
|
420
|
+
"CHROME_PUBLISHER_ID",
|
|
421
|
+
"CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL",
|
|
422
|
+
"CHROME_SERVICE_ACCOUNT_PRIVATE_KEY",
|
|
423
|
+
];
|
|
424
|
+
const FIREFOX_SECRETS = ["FIREFOX_EXTENSION_ID", "FIREFOX_JWT_ISSUER", "FIREFOX_JWT_SECRET"];
|
|
425
|
+
const EDGE_SECRETS = ["EDGE_PRODUCT_ID", "EDGE_CLIENT_ID", "EDGE_API_KEY"];
|
|
426
|
+
const ASC_SECRETS = ["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_PRIVATE_KEY"];
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Every name any row can consume, selected or not: the release's retraction step runs against
|
|
430
|
+
* the previous tag's selection, so its environment carries them all (an unset one is empty).
|
|
431
|
+
*/
|
|
432
|
+
export const RELEASE_SECRET_NAMES = [
|
|
433
|
+
"NPM_TOKEN",
|
|
434
|
+
"CLAWHUB_TOKEN",
|
|
435
|
+
...CHROME_SECRETS,
|
|
436
|
+
...FIREFOX_SECRETS,
|
|
437
|
+
...EDGE_SECRETS,
|
|
438
|
+
...ASC_SECRETS,
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Every registry row whose surfaces are all selected, with the exact upstream command for each
|
|
443
|
+
* column. Four consumers read this and nothing else: `secrets status`, `secrets check`, the
|
|
444
|
+
* release legs' presence gating, and the unpublish step.
|
|
445
|
+
*/
|
|
446
|
+
export function registries(project: Project): Registry[] {
|
|
447
|
+
const name = project.identity.name;
|
|
448
|
+
const version = project.identity.version ?? "0.0.0";
|
|
449
|
+
const pkg = npmName(project);
|
|
450
|
+
const pypi = pypiName(project);
|
|
451
|
+
const owner = githubOwner(project.identity.repository)?.toLowerCase();
|
|
452
|
+
const slug = githubSlug(project.identity.repository);
|
|
453
|
+
const server = owner ? `io.github.${owner}/${name}` : undefined;
|
|
454
|
+
const registry = "https://registry.modelcontextprotocol.io";
|
|
455
|
+
// The retraction's own message, on every registry that carries one: why the artifact stopped.
|
|
456
|
+
const why = `${name} no longer publishes here: the surface was deselected.`;
|
|
457
|
+
const browserZip = (browser: "chrome" | "firefox" | "edge") =>
|
|
458
|
+
`${BROWSER_HOST_DIR}/.output/${zipName(project, browser)}`;
|
|
459
|
+
const wxtSubmit = `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt submit --dry-run`;
|
|
460
|
+
|
|
461
|
+
const rows: Registry[] = [
|
|
462
|
+
{
|
|
463
|
+
id: "npm",
|
|
464
|
+
surfaces: ["npm"],
|
|
465
|
+
secrets: ["NPM_TOKEN"],
|
|
466
|
+
url: "https://www.npmjs.com/settings/~/tokens",
|
|
467
|
+
probe: npmAuthed("npm whoami"),
|
|
468
|
+
exists: `npm view ${pkg}@${version} version >/dev/null 2>&1`,
|
|
469
|
+
// An existing package is only eligible for trusted publishing; it does not prove the
|
|
470
|
+
// relationship was configured. OIDC is enabled explicitly after `npm trust` succeeds;
|
|
471
|
+
// otherwise a token remains a valid fallback, including for the first publish.
|
|
472
|
+
gate: `[ "$NPM_TRUSTED_PUBLISHER" = true ] || [ -n "$NPM_TOKEN" ]`,
|
|
473
|
+
confirmVariable: "NPM_TRUSTED_PUBLISHER",
|
|
474
|
+
// Reversible (an empty message undeprecates) and it never breaks an install, which
|
|
475
|
+
// `npm unpublish` does; the destructive form is behind `--hard`.
|
|
476
|
+
retract: npmAuthed(`npm deprecate ${pkg}@'*' "${why}"`),
|
|
477
|
+
retractHard: npmAuthed(`npm unpublish ${pkg} --force`),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "pypi",
|
|
481
|
+
surfaces: ["pypi"],
|
|
482
|
+
secrets: [],
|
|
483
|
+
url: "https://pypi.org/manage/account/publishing/",
|
|
484
|
+
exists: `curl -fsS -o /dev/null https://pypi.org/pypi/${pypi}/${version}/json`,
|
|
485
|
+
// A project that has published once proves its trusted publisher converted; before that the
|
|
486
|
+
// pending publisher is a web-only step, so a human confirms it with the variable.
|
|
487
|
+
gate: `curl -fsS -o /dev/null https://pypi.org/pypi/${pypi}/json || [ -n "$PYPI_TRUSTED_PUBLISHER" ]`,
|
|
488
|
+
// Yanking and deleting are dashboard actions: PyPI's only write API is Upload.
|
|
489
|
+
retractUrl: `https://pypi.org/manage/project/${pypi}/releases/`,
|
|
490
|
+
confirmVariable: "PYPI_TRUSTED_PUBLISHER",
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: "oci",
|
|
494
|
+
surfaces: ["mcp-registry"],
|
|
495
|
+
secrets: [],
|
|
496
|
+
// Publishing rides GITHUB_TOKEN's `packages: write`; deleting needs `delete:packages`,
|
|
497
|
+
// which that token does not carry, so retraction reads a PAT from the environment.
|
|
498
|
+
retractSecrets: ["GH_TOKEN"],
|
|
499
|
+
url: "https://github.com/settings/packages",
|
|
500
|
+
exists: owner
|
|
501
|
+
? `token=$(curl -fsS "https://ghcr.io/token?scope=repository:${owner}/${name}:pull&service=ghcr.io" | sed -n 's/.*"token":"\\([^"]*\\)".*/\\1/p') && curl -fsS -o /dev/null -H "Authorization: Bearer $token" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" "https://ghcr.io/v2/${owner}/${name}/manifests/${version}"`
|
|
502
|
+
: undefined,
|
|
503
|
+
// GITHUB_TOKEN is always there, so the leg always runs; what it cannot do is make the image
|
|
504
|
+
// public, and GitHub has no API for that at all (verified: the Packages REST surface is
|
|
505
|
+
// GET/DELETE/restore only), so the gate says so once per release instead of failing.
|
|
506
|
+
gate: `echo "::notice::ghcr.io publishes a private image on the first push whatever the repository's visibility; make it public once at https://github.com/${owner ?? "<owner>"}?tab=packages -> Package settings -> Danger Zone."`,
|
|
507
|
+
retract: owner
|
|
508
|
+
? `gh api --method DELETE /user/packages/container/${name} || gh api --method DELETE /orgs/${owner}/packages/container/${name} || echo "::notice::ghcr: delete the container package by hand at https://github.com/${owner}?tab=packages (a public package past 5,000 downloads needs GitHub support)."`
|
|
509
|
+
: undefined,
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "mcp-registry",
|
|
513
|
+
surfaces: ["mcp-registry"],
|
|
514
|
+
secrets: [],
|
|
515
|
+
url: registry,
|
|
516
|
+
exists: server
|
|
517
|
+
? `curl -fsS -o /dev/null "${registry}/v0/servers/${encodeURIComponent(server)}/versions/${version}"`
|
|
518
|
+
: undefined,
|
|
519
|
+
// mcp-publisher validates that every `packages[]` entry already exists in its own registry,
|
|
520
|
+
// so this leg can only run when each package leg is going to.
|
|
521
|
+
retract: server
|
|
522
|
+
? `${MCP_PUBLISHER_FETCH} && ./mcp-publisher login github-oidc && ./mcp-publisher status --status deleted --all-versions --message "${why}" ${server}`
|
|
523
|
+
: undefined,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: "clawhub-package",
|
|
527
|
+
surfaces: ["clawhub", "openclaw-native"],
|
|
528
|
+
secrets: ["CLAWHUB_TOKEN"],
|
|
529
|
+
url: "https://clawhub.ai",
|
|
530
|
+
probe: clawhubAuthed("whoami"),
|
|
531
|
+
exists: clawhubExists(
|
|
532
|
+
`package inspect ${projectName.openclawPackage(name)} --version ${version} --json`,
|
|
533
|
+
),
|
|
534
|
+
gate: allSet(["CLAWHUB_TOKEN"]),
|
|
535
|
+
// Soft: `package undelete` restores it, and the version number stays reserved either way.
|
|
536
|
+
retract: clawhubAuthed(`package delete ${projectName.openclawPackage(name)} --yes --json`),
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: "clawhub-skill",
|
|
540
|
+
surfaces: ["clawhub", "skill"],
|
|
541
|
+
secrets: ["CLAWHUB_TOKEN"],
|
|
542
|
+
url: "https://clawhub.ai",
|
|
543
|
+
probe: clawhubAuthed("whoami"),
|
|
544
|
+
exists: clawhubExists(`inspect ${name} --version ${version} --json`),
|
|
545
|
+
gate: allSet(["CLAWHUB_TOKEN"]),
|
|
546
|
+
retract: clawhubAuthed(`delete ${name} --yes --reason "${why}"`),
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
id: "pages",
|
|
550
|
+
surfaces: ["web"],
|
|
551
|
+
secrets: [],
|
|
552
|
+
retractSecrets: ["GH_TOKEN"],
|
|
553
|
+
url: slug ? `https://github.com/${slug}/settings/pages` : "https://docs.github.com/en/pages",
|
|
554
|
+
// Pages must be enabled with Source = GitHub Actions before `configure-pages` can run; a
|
|
555
|
+
// workflow token can read that (`pages: read`) but never set it — `bootstrap-repo` does.
|
|
556
|
+
gate: slug ? `gh api repos/${slug}/pages >/dev/null 2>&1` : undefined,
|
|
557
|
+
exists: slug
|
|
558
|
+
? `curl -fsS -o /dev/null https://${slug.replace("/", ".github.io/")}/`
|
|
559
|
+
: undefined,
|
|
560
|
+
// Doc-cited only: the docs require repository-admin or the Pages-settings permission and do
|
|
561
|
+
// not say whether a workflow token satisfies it, so a failure is a notice, not a red run.
|
|
562
|
+
retract: slug
|
|
563
|
+
? `gh api --method DELETE repos/${slug}/pages || echo "::notice::pages: disable the site by hand at https://github.com/${slug}/settings/pages"`
|
|
564
|
+
: undefined,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: "chrome",
|
|
568
|
+
surfaces: ["browser-extension"],
|
|
569
|
+
secrets: CHROME_SECRETS,
|
|
570
|
+
url: "https://chrome.google.com/webstore/devconsole",
|
|
571
|
+
// Web Store API v2's `fetchStatus` is the only read method and the only one `chromewebstore
|
|
572
|
+
// .readonly` reaches; `wxt submit --dry-run` calls it before it bails, so this is a real
|
|
573
|
+
// credential check (it is not on the v1.1 path, which bails first — hence the pinned v2).
|
|
574
|
+
probe: `CHROME_API_VERSION=v2 ${wxtSubmit} --chrome-zip ${browserZip("chrome")}`,
|
|
575
|
+
gate: allSet(CHROME_SECRETS),
|
|
576
|
+
// Google removed listing visibility from the API deliberately; it is dashboard-only.
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: "firefox",
|
|
580
|
+
surfaces: ["browser-extension"],
|
|
581
|
+
secrets: FIREFOX_SECRETS,
|
|
582
|
+
url: "https://addons.mozilla.org/developers/addon/api/key/",
|
|
583
|
+
// `GET /api/v5/addons/addon/{id}` under the JWT, before the dry run bails.
|
|
584
|
+
probe: `${wxtSubmit} --firefox-zip ${browserZip("firefox")} --firefox-sources-zip ${BROWSER_HOST_DIR}/.output/${sourcesZipName(project)}`,
|
|
585
|
+
exists: `curl -fsS -o /dev/null "https://addons.mozilla.org/api/v5/addons/addon/$FIREFOX_EXTENSION_ID/"`,
|
|
586
|
+
gate: allSet(FIREFOX_SECRETS),
|
|
587
|
+
// AMO's PATCH/DELETE exist but only over hand-rolled JWT HTTP, which C1 forbids.
|
|
588
|
+
retractUrl: "https://addons.mozilla.org/developers/addons",
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: "edge",
|
|
592
|
+
surfaces: ["browser-extension"],
|
|
593
|
+
secrets: EDGE_SECRETS,
|
|
594
|
+
url: "https://partner.microsoft.com/dashboard/microsoftedge/publishapi",
|
|
595
|
+
// No probe: the Edge API's only GETs need an operationId from a prior mutating call, and
|
|
596
|
+
// `wxt submit --dry-run` makes no network call at all for Edge (verified in its bundle).
|
|
597
|
+
gate: allSet(EDGE_SECRETS),
|
|
598
|
+
retractUrl: "https://partner.microsoft.com/dashboard/microsoftedge/overview",
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
id: "safari",
|
|
602
|
+
surfaces: ["browser-extension"],
|
|
603
|
+
secrets: ASC_SECRETS,
|
|
604
|
+
url: "https://appstoreconnect.apple.com/access/integrations/api",
|
|
605
|
+
// No probe here: the whole leg is macOS-only, and the ASC read needs an ES256-signed JWT,
|
|
606
|
+
// which is a hand-rolled HTTP client (C1).
|
|
607
|
+
gate: allSet(ASC_SECRETS),
|
|
608
|
+
retractUrl: "https://appstoreconnect.apple.com/apps",
|
|
609
|
+
},
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
const selected = rows.filter(
|
|
613
|
+
(row) =>
|
|
614
|
+
row.surfaces.every((surface) => has(project, surface)) &&
|
|
615
|
+
(row.id !== "safari" || project.tool.browserExtension?.safari === true),
|
|
616
|
+
);
|
|
617
|
+
const mcpRegistry = selected.find((row) => row.id === "mcp-registry");
|
|
618
|
+
if (mcpRegistry) {
|
|
619
|
+
const packages = selected.filter(
|
|
620
|
+
(row) => row.id === "npm" || row.id === "pypi" || row.id === "oci",
|
|
621
|
+
);
|
|
622
|
+
mcpRegistry.gate = packages.length
|
|
623
|
+
? packages.map((row) => `[ "$${gateVariable(row)}" = true ]`).join(" && ")
|
|
624
|
+
: "true";
|
|
625
|
+
}
|
|
626
|
+
return selected;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** The shell variable the release gate holds a row's presence in; the job output takes the same name. */
|
|
630
|
+
export function gateVariable(row: Registry): string {
|
|
631
|
+
return row.id.replace(/-/g, "_");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Every environment name a secret can be set under: sensitive config keys, then the release registries' tokens. */
|
|
635
|
+
export function secretsOf(project: Project): string[] {
|
|
636
|
+
const config = Object.entries(configProperties(project))
|
|
637
|
+
.filter(([, property]) => isSensitive(property))
|
|
638
|
+
.map(([key]) => envName(key));
|
|
639
|
+
const release = registries(project).flatMap((row) => row.secrets);
|
|
640
|
+
return [...new Set([...config, ...release])];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* The one-time human steps the table cannot automate — no API at all, or one no token in CI can
|
|
645
|
+
* reach. `secrets status` and `bootstrap-repo` print the same list.
|
|
646
|
+
*/
|
|
647
|
+
export function manualSteps(project: Project): string[] {
|
|
648
|
+
const rows = registries(project);
|
|
649
|
+
const has_ = (id: Registry["id"]) => rows.some((row) => row.id === id);
|
|
650
|
+
const slug = githubSlug(project.identity.repository);
|
|
651
|
+
const steps: string[] = [];
|
|
652
|
+
if (has_("oci")) {
|
|
653
|
+
steps.push(
|
|
654
|
+
`ghcr.io: the first push publishes a private image whatever the repository's visibility, and GitHub has no visibility API — flip it once at ${rows.find((row) => row.id === "oci")?.url}.`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (has_("pypi")) {
|
|
658
|
+
steps.push(
|
|
659
|
+
`PyPI: register a pending trusted publisher (repository, \`release.yml\`, environment \`pypi\`) at https://pypi.org/manage/account/publishing/, then \`gh variable set PYPI_TRUSTED_PUBLISHER -b true${slug ? ` -R ${slug}` : ""}\`.`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
if (has_("npm")) {
|
|
663
|
+
steps.push(
|
|
664
|
+
`npm: after the package exists, use npm@11.15+ from a logged-in 2FA session to run \`npm trust github ${npmName(project)} --file release.yml --repo ${slug ?? "<owner/repo>"} --allow-publish -y\`; granular access tokens with bypass 2FA are unsupported. After it succeeds, set \`gh variable set NPM_TRUSTED_PUBLISHER -b true${slug ? ` -R ${slug}` : ""}\`. Until then, NPM_TOKEN is the release fallback.`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
for (const id of ["chrome", "firefox", "edge", "safari"] as const) {
|
|
668
|
+
const row = rows.find((entry) => entry.id === id);
|
|
669
|
+
if (!row) continue;
|
|
670
|
+
steps.push(
|
|
671
|
+
`${id}: the listing itself — creating it, and unlisting it again — is a dashboard action with no API: ${row.retractUrl ?? row.url}.`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
steps.push(
|
|
675
|
+
"Curated directories are listed with their URLs in AGENTS.md's Listing section: each is a one-time human-reviewed submission, and removing one is a reverting pull request.",
|
|
676
|
+
);
|
|
677
|
+
return steps;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The retraction of every registry a dropped surface used to publish to, one step per row:
|
|
682
|
+
* `exists` → the secret guard → `retract`, so re-running the same tag is a no-op and a registry
|
|
683
|
+
* with no machine retraction prints the exact page instead of failing the release.
|
|
684
|
+
*
|
|
685
|
+
* `previous` is the project as the previous tag left it — its surfaces and its version — so the
|
|
686
|
+
* existence probes ask about what that tag actually published, not about the tag being cut now.
|
|
687
|
+
*/
|
|
688
|
+
export function unpublishSteps(
|
|
689
|
+
previous: Project,
|
|
690
|
+
dropped: readonly SurfaceId[],
|
|
691
|
+
hard = false,
|
|
692
|
+
): GateStep[] {
|
|
693
|
+
const version = previous.identity.version ?? "0.0.0";
|
|
694
|
+
return registries(previous)
|
|
695
|
+
.filter((row) => row.surfaces.some((surface) => dropped.includes(surface)))
|
|
696
|
+
.map((row) => {
|
|
697
|
+
const retract = (hard ? row.retractHard : undefined) ?? row.retract;
|
|
698
|
+
const where = row.retractUrl ?? row.url;
|
|
699
|
+
const guard = row.retractSecrets ?? row.secrets;
|
|
700
|
+
const body = !retract
|
|
701
|
+
? `echo "::notice::${row.id}: retract this listing by hand at ${where}"`
|
|
702
|
+
: guard.length
|
|
703
|
+
? `if ${allSet(guard)}; then ${retract}; else echo "::notice::${row.id}: set ${guard.join(", ")} to retract automatically, or do it by hand at ${where}"; fi`
|
|
704
|
+
: retract;
|
|
705
|
+
return {
|
|
706
|
+
name: `unpublish ${row.id}`,
|
|
707
|
+
run: row.exists
|
|
708
|
+
? `if ${row.exists}; then ${body}; else echo "${row.id}: nothing published at ${version}"; fi`
|
|
709
|
+
: body,
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** The one line CI runs, exactly as `toolfactory validate` is one gate step: the whole diff-and-retract. */
|
|
715
|
+
export function unpublishStep(project: Project): GateStep {
|
|
716
|
+
return { name: "unpublish dropped registries", run: `${toolfactoryCli(project)} unpublish` };
|
|
717
|
+
}
|