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/src/project/gate.ts
CHANGED
|
@@ -9,6 +9,18 @@
|
|
|
9
9
|
* A step here is a command and nothing else. GitHub Actions `uses:` steps are runner provisioning
|
|
10
10
|
* (checkout, toolchains, caches) and stay in `src/surfaces/workflows.ts`, which wraps this list.
|
|
11
11
|
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type NativePackageId,
|
|
15
|
+
nativePackageSteps,
|
|
16
|
+
nativeRegistryRows,
|
|
17
|
+
nativeToolchainSteps,
|
|
18
|
+
} from "../distribution/native.js";
|
|
19
|
+
import {
|
|
20
|
+
type SystemPackageId,
|
|
21
|
+
systemPackageSteps,
|
|
22
|
+
systemRegistryRows,
|
|
23
|
+
} from "../distribution/system.js";
|
|
12
24
|
import { githubSlug } from "../hosts/github.js";
|
|
13
25
|
import { githubOwner, projectName } from "../identity/name.js";
|
|
14
26
|
import { isInstructionOnly, type PackageManager, type Project, type SurfaceId } from "../model.js";
|
|
@@ -28,6 +40,14 @@ import {
|
|
|
28
40
|
npmName,
|
|
29
41
|
pypiName,
|
|
30
42
|
} from "../surfaces/shared.js";
|
|
43
|
+
import {
|
|
44
|
+
extensionName,
|
|
45
|
+
OVSX_VERSION,
|
|
46
|
+
publisher,
|
|
47
|
+
VSCE_VERSION,
|
|
48
|
+
HOST_DIR as VSCODE_HOST_DIR,
|
|
49
|
+
vsixName,
|
|
50
|
+
} from "../surfaces/vscode-extension.js";
|
|
31
51
|
|
|
32
52
|
export interface GateStep {
|
|
33
53
|
/** Step label: the CI step name, and the banner the shell script echoes. */
|
|
@@ -121,11 +141,18 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
121
141
|
const pm = commands(project);
|
|
122
142
|
const cli = toolfactoryCli(project);
|
|
123
143
|
const openclaw = has(project, "openclaw-native");
|
|
124
|
-
const steps: GateStep[] = [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
const steps: GateStep[] = [...bootstrapSteps(project)];
|
|
145
|
+
if (!typescript && has(project, "hermes-native")) {
|
|
146
|
+
// Hermes' launcher clears PYTHONPATH and runs its own venv. Its plugin dependency
|
|
147
|
+
// declaration is advisory, so Doctor can import a native Python core only after the
|
|
148
|
+
// project is installed into that interpreter. Run after bootstrap's `uv sync` on CI.
|
|
149
|
+
steps.push({
|
|
150
|
+
name: "Install core into Hermes",
|
|
151
|
+
run: 'uv pip install --python "$HOME/.hermes/hermes-agent/venv/bin/python" --editable .',
|
|
152
|
+
when: "ci",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
steps.push({ name: "toolfactory check", run: `${cli} check` }, outputsStep(project));
|
|
129
156
|
if (has(project, "claude")) {
|
|
130
157
|
steps.push({
|
|
131
158
|
name: "Install Claude Code CLI",
|
|
@@ -133,7 +160,11 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
133
160
|
when: "ci",
|
|
134
161
|
});
|
|
135
162
|
}
|
|
136
|
-
if (
|
|
163
|
+
if (
|
|
164
|
+
has(project, "web") ||
|
|
165
|
+
has(project, "browser-extension") ||
|
|
166
|
+
has(project, "vscode-extension")
|
|
167
|
+
) {
|
|
137
168
|
// The web smoke and the browser extension's own Playwright smoke both drive Chromium;
|
|
138
169
|
// their own npm installs fetch the browser, the runner needs its system libraries.
|
|
139
170
|
steps.push({
|
|
@@ -142,6 +173,7 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
142
173
|
when: "ci",
|
|
143
174
|
});
|
|
144
175
|
}
|
|
176
|
+
steps.push(...nativeToolchainSteps(project));
|
|
145
177
|
steps.push({
|
|
146
178
|
name: "toolfactory validate",
|
|
147
179
|
run: `${cli} validate`,
|
|
@@ -153,6 +185,7 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
153
185
|
name: "author tests",
|
|
154
186
|
run: typescript ? pm.test : "uv run --with pytest pytest -q",
|
|
155
187
|
});
|
|
188
|
+
if (typescript) steps.push({ name: "author integration tests", run: pm.run("test:integration") });
|
|
156
189
|
if (openclaw) {
|
|
157
190
|
// T3, credential-free: one real OpenClaw agent turn against a scripted model. The suite only
|
|
158
191
|
// exists when `tool.json` `tests.examples` names an operation the plugin carries, which is
|
|
@@ -213,13 +246,16 @@ const LEGAL_BUNDLE_PATHS = [
|
|
|
213
246
|
* the release workflow's `package` job, which uploads the directory as one artifact. Publishing
|
|
214
247
|
* to a registry stays a CI concern; producing the artifacts never is.
|
|
215
248
|
*/
|
|
216
|
-
export function packageSteps(project: Project): GateStep[] {
|
|
249
|
+
export function packageSteps(project: Project, options: { system?: boolean } = {}): GateStep[] {
|
|
217
250
|
const cli = toolfactoryCli(project);
|
|
218
251
|
const name = project.identity.name;
|
|
219
252
|
const steps: GateStep[] = [
|
|
220
253
|
{ name: "release directory", run: `rm -rf ${RELEASE_DIR} && mkdir -p ${RELEASE_DIR}` },
|
|
221
254
|
...bootstrapSteps(project),
|
|
222
255
|
outputsStep(project),
|
|
256
|
+
...nativeToolchainSteps(project),
|
|
257
|
+
...nativePackageSteps(project),
|
|
258
|
+
...systemPackageSteps(project, { include: options.system !== false }),
|
|
223
259
|
];
|
|
224
260
|
if (has(project, "web")) {
|
|
225
261
|
steps.push({
|
|
@@ -232,27 +268,35 @@ export function packageSteps(project: Project): GateStep[] {
|
|
|
232
268
|
if (has(project, "npm")) {
|
|
233
269
|
steps.push({ name: "npm tarball", run: `npm pack --pack-destination ${RELEASE_DIR}` });
|
|
234
270
|
}
|
|
271
|
+
if (has(project, "pypi")) {
|
|
272
|
+
// Keep the wheel and sdist apart from the release's npm tarballs, plugin bundles and web
|
|
273
|
+
// archive: PyPA's publisher accepts only Python distribution files. The MCPB stage below
|
|
274
|
+
// unpacks this same canonical sdist rather than reproducing its source-file selection.
|
|
275
|
+
steps.push({ name: "python distributions", run: `uv build --out-dir ${RELEASE_DIR}/pypi` });
|
|
276
|
+
}
|
|
235
277
|
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
278
|
const stage = "dist/mcpb";
|
|
241
|
-
const tarball = npmTarball(project);
|
|
242
279
|
steps.push({
|
|
243
280
|
name: "MCPB bundle",
|
|
244
|
-
run:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
281
|
+
run:
|
|
282
|
+
project.tool.binding === "python"
|
|
283
|
+
? [
|
|
284
|
+
`rm -rf ${stage} && mkdir -p ${stage}`,
|
|
285
|
+
`tar -xzf ${RELEASE_DIR}/pypi/*.tar.gz -C ${stage} --strip-components=1`,
|
|
286
|
+
`cp ${MCPB_MANIFEST_PATH} ${stage}/manifest.json`,
|
|
287
|
+
`npx -y @anthropic-ai/mcpb@${MCPB_PIN} pack ${stage} ${RELEASE_DIR}/${name}.mcpb`,
|
|
288
|
+
].join(" && ")
|
|
289
|
+
: [
|
|
290
|
+
// The Node bundle root is the published package with production dependencies
|
|
291
|
+
// installed, so whatever ships to npm is exactly what ships to Claude Desktop.
|
|
292
|
+
`rm -rf ${stage} && mkdir -p ${stage}`,
|
|
293
|
+
`tar -xzf ${RELEASE_DIR}/${npmTarball(project)} -C ${stage} --strip-components=1`,
|
|
294
|
+
`npm --prefix ${stage} install --omit=dev --ignore-scripts`,
|
|
295
|
+
`cp ${MCPB_MANIFEST_PATH} ${stage}/manifest.json`,
|
|
296
|
+
`npx -y @anthropic-ai/mcpb@${MCPB_PIN} pack ${stage} ${RELEASE_DIR}/${name}.mcpb`,
|
|
297
|
+
].join(" && "),
|
|
251
298
|
});
|
|
252
299
|
}
|
|
253
|
-
if (has(project, "pypi")) {
|
|
254
|
-
steps.push({ name: "python distributions", run: `uv build --out-dir ${RELEASE_DIR}` });
|
|
255
|
-
}
|
|
256
300
|
if (has(project, "openclaw-native")) {
|
|
257
301
|
// ClawHub's reusable workflow has no build step of its own, so the tarball it publishes has
|
|
258
302
|
// to arrive already built (`package_artifact_name`).
|
|
@@ -314,6 +358,12 @@ export function packageSteps(project: Project): GateStep[] {
|
|
|
314
358
|
});
|
|
315
359
|
}
|
|
316
360
|
// COVERAGE.md is tracked; coverage.json is a build output that need not be, so recompute it.
|
|
361
|
+
if (has(project, "vscode-extension")) {
|
|
362
|
+
steps.push({
|
|
363
|
+
name: "VS Code extension VSIX",
|
|
364
|
+
run: `npm --prefix ${VSCODE_HOST_DIR} install && npm --prefix ${VSCODE_HOST_DIR} run vsix && cp ${VSCODE_HOST_DIR}/extension.vsix ${RELEASE_DIR}/${vsixName(project)}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
317
367
|
steps.push({
|
|
318
368
|
name: "coverage report",
|
|
319
369
|
run: `cp COVERAGE.md ${RELEASE_DIR}/ && ${cli} coverage > ${RELEASE_DIR}/coverage.json`,
|
|
@@ -340,6 +390,8 @@ export function openclawTarball(project: Project): string {
|
|
|
340
390
|
export interface Registry {
|
|
341
391
|
/** Stable id: the release job suffix, the `secrets status` row, the unpublish step name. */
|
|
342
392
|
id:
|
|
393
|
+
| NativePackageId
|
|
394
|
+
| SystemPackageId
|
|
343
395
|
| "npm"
|
|
344
396
|
| "pypi"
|
|
345
397
|
| "mcp-registry"
|
|
@@ -350,7 +402,9 @@ export interface Registry {
|
|
|
350
402
|
| "chrome"
|
|
351
403
|
| "firefox"
|
|
352
404
|
| "edge"
|
|
353
|
-
| "safari"
|
|
405
|
+
| "safari"
|
|
406
|
+
| "vscode-marketplace"
|
|
407
|
+
| "open-vsx";
|
|
354
408
|
/** The surfaces whose selection publishes here; all must be selected. */
|
|
355
409
|
surfaces: SurfaceId[];
|
|
356
410
|
/** Environment names the publish leg consumes (repository scope); [] for OIDC/GITHUB_TOKEN legs. */
|
|
@@ -432,6 +486,22 @@ const ASC_SECRETS = ["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_PRIVATE_KEY"];
|
|
|
432
486
|
export const RELEASE_SECRET_NAMES = [
|
|
433
487
|
"NPM_TOKEN",
|
|
434
488
|
"CLAWHUB_TOKEN",
|
|
489
|
+
"VSCE_PAT",
|
|
490
|
+
"OVSX_PAT",
|
|
491
|
+
"CARGO_REGISTRY_TOKEN",
|
|
492
|
+
"NUGET_API_KEY",
|
|
493
|
+
"MAVEN_CENTRAL_USERNAME",
|
|
494
|
+
"MAVEN_CENTRAL_PASSWORD",
|
|
495
|
+
"MAVEN_GPG_PRIVATE_KEY",
|
|
496
|
+
"MAVEN_GPG_PASSPHRASE",
|
|
497
|
+
"RUBYGEMS_API_KEY",
|
|
498
|
+
"HOMEBREW_TAP_TOKEN",
|
|
499
|
+
"SCOOP_BUCKET_TOKEN",
|
|
500
|
+
"CHOCOLATEY_API_KEY",
|
|
501
|
+
"APT_GPG_PRIVATE_KEY",
|
|
502
|
+
"APT_GPG_KEY_ID",
|
|
503
|
+
"COPR_LOGIN",
|
|
504
|
+
"COPR_TOKEN",
|
|
435
505
|
...CHROME_SECRETS,
|
|
436
506
|
...FIREFOX_SECRETS,
|
|
437
507
|
...EDGE_SECRETS,
|
|
@@ -459,6 +529,29 @@ export function registries(project: Project): Registry[] {
|
|
|
459
529
|
const wxtSubmit = `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt submit --dry-run`;
|
|
460
530
|
|
|
461
531
|
const rows: Registry[] = [
|
|
532
|
+
...nativeRegistryRows(project),
|
|
533
|
+
...systemRegistryRows(project),
|
|
534
|
+
{
|
|
535
|
+
id: "vscode-marketplace",
|
|
536
|
+
surfaces: ["vscode-extension"],
|
|
537
|
+
secrets: ["VSCE_PAT"],
|
|
538
|
+
url: "https://marketplace.visualstudio.com/manage",
|
|
539
|
+
probe: `npx --yes @vscode/vsce@${VSCE_VERSION} verify-pat ${publisher(project)}`,
|
|
540
|
+
exists: `npx --yes @vscode/vsce@${VSCE_VERSION} show ${publisher(project)}.${extensionName(project)} --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.exit(JSON.parse(s).versions.some(v=>v.version==="${version}")?0:1))'`,
|
|
541
|
+
gate: allSet(["VSCE_PAT"]),
|
|
542
|
+
retractUrl: "https://marketplace.visualstudio.com/manage",
|
|
543
|
+
retractHard: `npx --yes @vscode/vsce@${VSCE_VERSION} unpublish ${publisher(project)}.${extensionName(project)} --force`,
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "open-vsx",
|
|
547
|
+
surfaces: ["vscode-extension"],
|
|
548
|
+
secrets: ["OVSX_PAT"],
|
|
549
|
+
url: "https://open-vsx.org/user-settings/tokens",
|
|
550
|
+
probe: `npx --yes ovsx@${OVSX_VERSION} verify-pat ${publisher(project)}`,
|
|
551
|
+
exists: `curl -fsS -o /dev/null https://open-vsx.org/api/${publisher(project)}/${extensionName(project)}/${version}`,
|
|
552
|
+
gate: allSet(["OVSX_PAT"]),
|
|
553
|
+
retractUrl: "https://open-vsx.org/user-settings/extensions",
|
|
554
|
+
},
|
|
462
555
|
{
|
|
463
556
|
id: "npm",
|
|
464
557
|
surfaces: ["npm"],
|
|
@@ -612,7 +705,9 @@ export function registries(project: Project): Registry[] {
|
|
|
612
705
|
const selected = rows.filter(
|
|
613
706
|
(row) =>
|
|
614
707
|
row.surfaces.every((surface) => has(project, surface)) &&
|
|
615
|
-
(row.id !== "safari" || project.tool.browserExtension?.safari === true)
|
|
708
|
+
(row.id !== "safari" || project.tool.browserExtension?.safari === true) &&
|
|
709
|
+
(row.id !== "vscode-marketplace" || project.tool.vscode?.marketplace !== false) &&
|
|
710
|
+
(row.id !== "open-vsx" || project.tool.vscode?.openvsx !== false),
|
|
616
711
|
);
|
|
617
712
|
const mcpRegistry = selected.find((row) => row.id === "mcp-registry");
|
|
618
713
|
if (mcpRegistry) {
|
|
@@ -623,6 +718,14 @@ export function registries(project: Project): Registry[] {
|
|
|
623
718
|
? packages.map((row) => `[ "$${gateVariable(row)}" = true ]`).join(" && ")
|
|
624
719
|
: "true";
|
|
625
720
|
}
|
|
721
|
+
const runtime = selected.find(
|
|
722
|
+
(row) => row.id === (project.tool.binding === "python" ? "pypi" : "npm"),
|
|
723
|
+
);
|
|
724
|
+
for (const row of selected.filter(
|
|
725
|
+
(row) => row.id === "vscode-marketplace" || row.id === "open-vsx",
|
|
726
|
+
)) {
|
|
727
|
+
if (runtime) row.gate = `(${row.gate}) && { ${runtime.exists} || { ${runtime.gate}; }; }`;
|
|
728
|
+
}
|
|
626
729
|
return selected;
|
|
627
730
|
}
|
|
628
731
|
|
|
@@ -649,6 +752,10 @@ export function manualSteps(project: Project): string[] {
|
|
|
649
752
|
const has_ = (id: Registry["id"]) => rows.some((row) => row.id === id);
|
|
650
753
|
const slug = githubSlug(project.identity.repository);
|
|
651
754
|
const steps: string[] = [];
|
|
755
|
+
if (has_("vscode-marketplace") || has_("open-vsx"))
|
|
756
|
+
steps.push(
|
|
757
|
+
"VS Code: register the publisher in Visual Studio Marketplace; create the Open VSX namespace and accept its publisher agreement before setting VSCE_PAT / OVSX_PAT. tool.vscode.publisher selects that identity.",
|
|
758
|
+
);
|
|
652
759
|
if (has_("oci")) {
|
|
653
760
|
steps.push(
|
|
654
761
|
`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}.`,
|
package/src/project/lock.ts
CHANGED
|
@@ -23,6 +23,18 @@ export const LockSchema = z.object({
|
|
|
23
23
|
* patch — or deselecting the surface that added it — uninstalls it instead of stranding it.
|
|
24
24
|
*/
|
|
25
25
|
keys: z.array(z.string()).optional(),
|
|
26
|
+
/** A keyed array's inverse: the entries Toolfactory owns at each dotted path. */
|
|
27
|
+
keyedArrays: z
|
|
28
|
+
.record(
|
|
29
|
+
z.string(),
|
|
30
|
+
z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
values: z.array(z.string()),
|
|
33
|
+
/** The last projected entry shape lets a later plan remove only stale generated leaves. */
|
|
34
|
+
entries: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
.optional(),
|
|
26
38
|
/**
|
|
27
39
|
* A region file's inverse, exactly as `keys` is a merge file's: the marker pairs its
|
|
28
40
|
* projector wrote. Regions the current plan no longer writes are emptied before the current
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Validation is Ajv against the cached official schemas; the spec forbids fetching at load.
|
|
5
5
|
*/
|
|
6
6
|
import type { Project, Surface } from "../model.js";
|
|
7
|
-
import {
|
|
7
|
+
import { kernelLaunch, mcpVerdict } from "./shared.js";
|
|
8
8
|
|
|
9
9
|
export const MCP_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
10
10
|
export const PLUGIN_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
@@ -24,12 +24,13 @@ export const surface: Surface = {
|
|
|
24
24
|
const launch = kernelLaunch(project, "${PLUGIN_ROOT}");
|
|
25
25
|
return [
|
|
26
26
|
{
|
|
27
|
-
kind: "
|
|
27
|
+
kind: "merge",
|
|
28
28
|
path: "mcp.json",
|
|
29
|
-
|
|
29
|
+
format: "json",
|
|
30
|
+
patch: {
|
|
30
31
|
$schema: MCP_SCHEMA_ID,
|
|
31
32
|
mcpServers: { [project.identity.name]: { type: "stdio", ...launch } },
|
|
32
|
-
}
|
|
33
|
+
},
|
|
33
34
|
},
|
|
34
35
|
];
|
|
35
36
|
},
|
|
@@ -129,6 +129,22 @@ describe("agents", () => {
|
|
|
129
129
|
expect(setup).toContain("npm install --no-audit --no-fund");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it("installs uv before Python projects run the vendored setup workflow", () => {
|
|
133
|
+
const workflow = planned(
|
|
134
|
+
project(["cli"], { binding: "python" }),
|
|
135
|
+
".github/workflows/agent-config.yml",
|
|
136
|
+
);
|
|
137
|
+
if (workflow.kind !== "file") throw new Error("expected an agent-config workflow file");
|
|
138
|
+
const install = "uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0";
|
|
139
|
+
expect(workflow.content).toContain(install);
|
|
140
|
+
expect(workflow.content.indexOf(install)).toBeLessThan(
|
|
141
|
+
workflow.content.indexOf("run: bash .agents/setup --all"),
|
|
142
|
+
);
|
|
143
|
+
const typescript = planned(project(["cli"]), ".github/workflows/agent-config.yml");
|
|
144
|
+
if (typescript.kind !== "file") throw new Error("expected an agent-config workflow file");
|
|
145
|
+
expect(typescript.content).toBe(TEMPLATE_FILES[".github/workflows/agent-config.yml"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
132
148
|
it("owns the agent adapters, the binding's ignores and the build outputs in .gitignore", () => {
|
|
133
149
|
const bare = region(project(["cli"]), IGNORE_PATH);
|
|
134
150
|
const agentBlocks = (TEMPLATE_FILES[IGNORE_PATH] ?? "").split("# ─── Secrets")[0] ?? "";
|
package/src/surfaces/agents.ts
CHANGED
|
@@ -89,6 +89,13 @@ export const RELOAD: Reload[] = [
|
|
|
89
89
|
"`hermes skills trust` once in this repo, then a new conversation: the resolved skill directories are stable for a conversation.",
|
|
90
90
|
line: "Run `hermes skills trust` once in this repo, then start a new Hermes conversation — every invocation re-scans plugins, skills and AGENTS.md. `/reload-mcp` only affects an already-open session; `hermes gateway restart` is only for the messaging gateway.",
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
harness: "GSD",
|
|
94
|
+
env: [],
|
|
95
|
+
mcp: "`python3 .agents/sync.py --all` renders the shared `.mcp.json` even on a GSD-only machine. Refresh with `mcp_servers(refresh=true)`; start a new session after changing a connected server. Approve `mcp_discover` interactively once before unattended stdio use.",
|
|
96
|
+
instructions:
|
|
97
|
+
"Reads `AGENTS.md` and `.agents/skills/` natively; `/reload` refreshes resources. `--bare` omits them.",
|
|
98
|
+
},
|
|
92
99
|
{
|
|
93
100
|
harness: "Gemini CLI",
|
|
94
101
|
env: [],
|
|
@@ -191,7 +198,8 @@ function layoutSection(project: Project): string[] {
|
|
|
191
198
|
has(project, "openclaw-native") ||
|
|
192
199
|
has(project, "hermes-native") ||
|
|
193
200
|
has(project, "dsh") ||
|
|
194
|
-
has(project, "browser-extension")
|
|
201
|
+
has(project, "browser-extension") ||
|
|
202
|
+
has(project, "vscode-extension")
|
|
195
203
|
) {
|
|
196
204
|
lines.push(
|
|
197
205
|
"- `hosts/<id>/` — the host-native escape hatch for a selected host; nothing else creates it.",
|
|
@@ -274,6 +282,15 @@ function installSection(project: Project): string[] {
|
|
|
274
282
|
" KEY/PASSWORD/SECRET/TOKEN names before spawning an MCP server.",
|
|
275
283
|
);
|
|
276
284
|
}
|
|
285
|
+
if (has(project, "vscode-extension")) {
|
|
286
|
+
lines.push(
|
|
287
|
+
"- **VS Code extension**: `npm --prefix hosts/vscode install`, then open `hosts/vscode` in VS Code",
|
|
288
|
+
" and press F5. Development and `npm --prefix hosts/vscode test` use the checkout's kernel.",
|
|
289
|
+
" `npm --prefix hosts/vscode run vsix` packages it; install with `code --install-extension hosts/vscode/extension.vsix`.",
|
|
290
|
+
" Use Developer: Reload Window after rebuilding an installed extension. Native activation, contributions",
|
|
291
|
+
" and every VS Code API are available in your own files; include your license in the extension directory.",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
277
294
|
if (has(project, "browser-extension")) {
|
|
278
295
|
const { command, args } = getBinding(project.tool.binding).cliCommand(project);
|
|
279
296
|
lines.push(
|
|
@@ -292,7 +309,8 @@ function installSection(project: Project): string[] {
|
|
|
292
309
|
!openclaw &&
|
|
293
310
|
!hermes &&
|
|
294
311
|
!has(project, "dsh") &&
|
|
295
|
-
!has(project, "browser-extension")
|
|
312
|
+
!has(project, "browser-extension") &&
|
|
313
|
+
!has(project, "vscode-extension")
|
|
296
314
|
) {
|
|
297
315
|
lines.push(
|
|
298
316
|
"_No host-native surface (`openclaw-native`, `hermes-native`, `dsh`, `browser-extension`) or",
|
|
@@ -681,6 +699,22 @@ function devcontainer(project: Project): string {
|
|
|
681
699
|
return `${JSON.stringify({ ...document, features }, null, 2)}\n`;
|
|
682
700
|
}
|
|
683
701
|
|
|
702
|
+
/** The vendor-owned workflow gains the Python prerequisite it cannot know. */
|
|
703
|
+
function agentConfigWorkflow(project: Project): string {
|
|
704
|
+
const path = ".github/workflows/agent-config.yml";
|
|
705
|
+
const workflow = TEMPLATE_FILES[path] ?? "";
|
|
706
|
+
if (isInstructionOnly(project.tool) || project.tool.binding !== "python") return workflow;
|
|
707
|
+
const setup = " - name: Run the shared setup entry point\n";
|
|
708
|
+
if (!workflow.includes(setup))
|
|
709
|
+
throw new Error(`${path} no longer has the setup step; re-vendor before adding uv.`);
|
|
710
|
+
return workflow.replace(
|
|
711
|
+
setup,
|
|
712
|
+
" - name: Install uv\n" +
|
|
713
|
+
" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0\n\n" +
|
|
714
|
+
setup,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
684
718
|
/** Whole-file carriers: no per-project content, `adopt` is the escape hatch. */
|
|
685
719
|
function vendored(project: Project): PlannedFile[] {
|
|
686
720
|
const skip = new Set([SETUP_PATH, IGNORE_PATH, ".devcontainer/devcontainer.json"]);
|
|
@@ -691,7 +725,8 @@ function vendored(project: Project): PlannedFile[] {
|
|
|
691
725
|
([path, content]): PlannedFile => ({
|
|
692
726
|
kind: "file",
|
|
693
727
|
path,
|
|
694
|
-
content
|
|
728
|
+
content:
|
|
729
|
+
path === ".github/workflows/agent-config.yml" ? agentConfigWorkflow(project) : content,
|
|
695
730
|
...(path.endsWith("sync.py") ? { mode: 0o755 } : {}),
|
|
696
731
|
}),
|
|
697
732
|
),
|
package/src/surfaces/claude.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
envName,
|
|
13
13
|
has,
|
|
14
14
|
isSensitive,
|
|
15
|
-
json,
|
|
16
15
|
kernelLaunch,
|
|
17
16
|
mcpVerdict,
|
|
18
17
|
requiredConfig,
|
|
@@ -79,8 +78,13 @@ export const surface: Surface = {
|
|
|
79
78
|
plugins: [compact({ name: identity.name, source: "./", description: identity.description })],
|
|
80
79
|
});
|
|
81
80
|
return [
|
|
82
|
-
{ kind: "
|
|
83
|
-
{
|
|
81
|
+
{ kind: "merge", path: ".claude-plugin/plugin.json", format: "json", patch: manifest },
|
|
82
|
+
{
|
|
83
|
+
kind: "merge",
|
|
84
|
+
path: ".claude-plugin/marketplace.json",
|
|
85
|
+
format: "json",
|
|
86
|
+
patch: marketplace,
|
|
87
|
+
},
|
|
84
88
|
];
|
|
85
89
|
},
|
|
86
90
|
validate(project) {
|
|
@@ -33,8 +33,8 @@ describe("codex", () => {
|
|
|
33
33
|
it("makes the repository its own single-plugin marketplace, and validates with the real codex CLI", () => {
|
|
34
34
|
const files = codex.plan(project(["codex", "mcp"]));
|
|
35
35
|
const marketplace = files.find((entry) => entry.path === ".agents/plugins/marketplace.json");
|
|
36
|
-
if (marketplace?.kind !== "
|
|
37
|
-
expect(
|
|
36
|
+
if (marketplace?.kind !== "merge") throw new Error("expected a merge manifest");
|
|
37
|
+
expect(marketplace.patch).toMatchObject({
|
|
38
38
|
name: "hello",
|
|
39
39
|
interface: { displayName: "Hello" },
|
|
40
40
|
plugins: [
|
|
@@ -64,8 +64,18 @@ describe("codex", () => {
|
|
|
64
64
|
const target = project(["skill", "codex"]);
|
|
65
65
|
target.tool.runtime = "none";
|
|
66
66
|
const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
|
|
67
|
-
if (manifest?.kind !== "
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
67
|
+
if (manifest?.kind !== "merge") throw new Error("expected a manifest");
|
|
68
|
+
expect(manifest.patch).toMatchObject({ skills: "./skills/" });
|
|
69
|
+
expect(manifest.patch).not.toHaveProperty("mcpServers");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("can omit the eager MCP server while retaining the plugin skill", () => {
|
|
73
|
+
const target = project(["skill", "codex", "mcp"]);
|
|
74
|
+
target.tool.codex = { mcp: false };
|
|
75
|
+
const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
|
|
76
|
+
if (manifest?.kind !== "merge") throw new Error("expected a manifest");
|
|
77
|
+
const parsed = manifest.patch;
|
|
78
|
+
expect(parsed.skills).toBe("./skills/");
|
|
79
|
+
expect(parsed).not.toHaveProperty("mcpServers");
|
|
70
80
|
});
|
|
71
81
|
});
|
package/src/surfaces/codex.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { projectName } from "../identity/name.js";
|
|
12
12
|
import type { Surface } from "../model.js";
|
|
13
|
-
import { compact, has,
|
|
13
|
+
import { compact, has, kernelLaunch, mcpVerdict } from "./shared.js";
|
|
14
14
|
|
|
15
15
|
/** The Codex CLI (`@openai/codex`) `validate()` is proven against — pin for a deterministic gate. */
|
|
16
16
|
export const CODEX_PIN = "0.152.1";
|
|
@@ -43,9 +43,10 @@ export const surface: Surface = {
|
|
|
43
43
|
license: identity.license,
|
|
44
44
|
keywords: identity.keywords,
|
|
45
45
|
skills: has(project, "skill") ? "./skills/" : undefined,
|
|
46
|
-
mcpServers:
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
mcpServers:
|
|
47
|
+
has(project, "mcp") && project.tool.codex?.mcp !== false
|
|
48
|
+
? { [identity.name]: kernelLaunch(project, "${PLUGIN_ROOT}") }
|
|
49
|
+
: undefined,
|
|
49
50
|
interface: codexInterface,
|
|
50
51
|
});
|
|
51
52
|
// `source: "local"` + `path: "."` is the marketplace root itself: one repository, one
|
|
@@ -64,8 +65,13 @@ export const surface: Surface = {
|
|
|
64
65
|
],
|
|
65
66
|
});
|
|
66
67
|
return [
|
|
67
|
-
{ kind: "
|
|
68
|
-
{
|
|
68
|
+
{ kind: "merge", path: ".codex-plugin/plugin.json", format: "json", patch: manifest },
|
|
69
|
+
{
|
|
70
|
+
kind: "merge",
|
|
71
|
+
path: ".agents/plugins/marketplace.json",
|
|
72
|
+
format: "json",
|
|
73
|
+
patch: marketplace,
|
|
74
|
+
},
|
|
69
75
|
];
|
|
70
76
|
},
|
|
71
77
|
validate(project) {
|
package/src/surfaces/cursor.ts
CHANGED
|
@@ -4,15 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { projectName } from "../identity/name.js";
|
|
6
6
|
import type { Surface } from "../model.js";
|
|
7
|
-
import {
|
|
8
|
-
compact,
|
|
9
|
-
configProperties,
|
|
10
|
-
envName,
|
|
11
|
-
has,
|
|
12
|
-
json,
|
|
13
|
-
mcpVerdict,
|
|
14
|
-
requiredConfig,
|
|
15
|
-
} from "./shared.js";
|
|
7
|
+
import { compact, configProperties, envName, has, mcpVerdict, requiredConfig } from "./shared.js";
|
|
16
8
|
|
|
17
9
|
export const surface: Surface = {
|
|
18
10
|
id: "cursor",
|
|
@@ -51,7 +43,7 @@ export const surface: Surface = {
|
|
|
51
43
|
mcpServers: has(project, "agent-plugins") ? "./mcp.json" : undefined,
|
|
52
44
|
variables,
|
|
53
45
|
});
|
|
54
|
-
return [{ kind: "
|
|
46
|
+
return [{ kind: "merge", path: ".cursor-plugin/plugin.json", format: "json", patch: manifest }];
|
|
55
47
|
},
|
|
56
48
|
verdict: (operation) => {
|
|
57
49
|
const verdict = mcpVerdict(operation);
|