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
package/dist/commands.js
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command layer. Every toolfactory verb is a function here so the CLI and the MCP
|
|
3
|
+
* surface expose exactly the same operations.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { parseEnv } from "node:util";
|
|
10
|
+
import { getBinding } from "./bindings/index.js";
|
|
11
|
+
import { LIVE_TEST_COMMAND } from "./bindings/python.js";
|
|
12
|
+
import { createRepo, githubSlug, LIVE_ENVIRONMENT, bootstrapRepo as prepareRepository, } from "./hosts/github.js";
|
|
13
|
+
import { assertValidName, projectName } from "./identity/name.js";
|
|
14
|
+
import { introspect as runIntrospect, snapshot } from "./introspect/index.js";
|
|
15
|
+
import { assertNoSensitiveArgument, defaultSurfaces, isInstructionOnly, SURFACE_IDS, ToolConfigSchema, } from "./model.js";
|
|
16
|
+
import { apply, check as checkPlan, setState } from "./project/apply.js";
|
|
17
|
+
import { gateSteps, manualSteps, PACKAGE_MANAGER_COMMANDS, packageSteps, registries, toolfactoryCli, unpublishSteps, } from "./project/gate.js";
|
|
18
|
+
import { droppedSurfaces, previousTag, toolAtRef } from "./project/history.js";
|
|
19
|
+
import { loadProject, OPS_PATH, TOOL_PATH, TOOLFACTORY_VERSION, toOperation, } from "./project/load.js";
|
|
20
|
+
import { readLock } from "./project/lock.js";
|
|
21
|
+
import { buildPlan, TOOL_SCHEMA_PATH } from "./project/plan.js";
|
|
22
|
+
import { computeCoverage } from "./report/coverage.js";
|
|
23
|
+
import { PLUGIN_SCHEMA_ID } from "./surfaces/agent-plugins.js";
|
|
24
|
+
import { reloadLine, SETUP_PATH } from "./surfaces/agents.js";
|
|
25
|
+
import { HOST_DIR as BROWSER_HOST_DIR } from "./surfaces/browser-extension.js";
|
|
26
|
+
import { assertSurfaceRequirements, getSurface, selectedSurfaces } from "./surfaces/registry.js";
|
|
27
|
+
import { compact, configProperties, envName, has, isSensitive, json, liveCredentials, requiredConfig, } from "./surfaces/shared.js";
|
|
28
|
+
import { validateAgentPlugin } from "./validate/agent-plugins.js";
|
|
29
|
+
const BUNDLE_SURFACES = ["agent-plugins", "skill", "claude", "codex", "cursor"];
|
|
30
|
+
function writeIfAbsent(root, files) {
|
|
31
|
+
const written = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
if (file.kind !== "file")
|
|
34
|
+
continue;
|
|
35
|
+
const path = join(root, file.path);
|
|
36
|
+
if (existsSync(path))
|
|
37
|
+
continue;
|
|
38
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
39
|
+
writeFileSync(path, file.content);
|
|
40
|
+
written.push(file.path);
|
|
41
|
+
}
|
|
42
|
+
return written;
|
|
43
|
+
}
|
|
44
|
+
function git(root, args) {
|
|
45
|
+
return spawnSync("git", args, { cwd: root, stdio: "ignore", timeout: 60_000 }).status === 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `git commit` refuses to run without an identity, and a CI runner or a fresh machine has none;
|
|
49
|
+
* this commit is toolfactory's own, so it falls back to naming itself rather than failing.
|
|
50
|
+
*/
|
|
51
|
+
function commitIdentity(root) {
|
|
52
|
+
return git(root, ["config", "--get", "user.email"])
|
|
53
|
+
? []
|
|
54
|
+
: ["-c", "user.name=toolfactory", "-c", "user.email=toolfactory@localhost"];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `.agents/setup`: the template's one entry point — it renders the harness adapters from
|
|
58
|
+
* `.agents/`, installs the git hooks that keep them in sync (and the drift gate in front of the
|
|
59
|
+
* pre-commit hook), and installs the project's dependencies. Best-effort: a machine missing
|
|
60
|
+
* python3, pnpm or the network still gets a scaffolded project, and `nextSteps` says to run it.
|
|
61
|
+
*/
|
|
62
|
+
function agentSetup(root) {
|
|
63
|
+
const result = spawnSync("bash", [SETUP_PATH], {
|
|
64
|
+
cwd: root,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
timeout: 300_000,
|
|
67
|
+
});
|
|
68
|
+
const harnesses = (result.stdout ?? "")
|
|
69
|
+
.split("\n")
|
|
70
|
+
.flatMap((line) => line.match(/^Generated ([^:]+):/)?.slice(1, 2) ?? []);
|
|
71
|
+
return { setup: result.status === 0, harnesses };
|
|
72
|
+
}
|
|
73
|
+
function nextSteps(project, agentConfig) {
|
|
74
|
+
const cli = toolfactoryCli(project);
|
|
75
|
+
const ops = project.tool.binding === "typescript"
|
|
76
|
+
? "src/ops.ts"
|
|
77
|
+
: `src/${projectName.pythonPackage(project.identity.name)}/ops.py`;
|
|
78
|
+
// The credential inventory, once, where the agent that ran `init` will read it: a project that
|
|
79
|
+
// declares none says nothing at all.
|
|
80
|
+
const summary = secretsSummary(secretsReport(project));
|
|
81
|
+
return [
|
|
82
|
+
agentConfig.setup
|
|
83
|
+
? `Agent config is wired: \`.agents/\` is the canon (\`skills/\`, \`mcp/servers.json\`). \`${SETUP_PATH}\` rendered ${agentConfig.harnesses.length} harness adapter(s) here (${agentConfig.harnesses.join(", ") || "none detected"}) and installed the pre-commit/post-checkout/post-merge hooks, so pulls, branch switches and commits re-sync on their own. Details: \`.agents/README.md\`.`
|
|
84
|
+
: `Agent config is written but \`${SETUP_PATH}\` did not finish here: run \`bash ${SETUP_PATH}\` to render the harness adapters from \`.agents/\`, install the git hooks that keep them in sync, and install the dependencies. Details: \`.agents/README.md\`.`,
|
|
85
|
+
reloadLine(process.env),
|
|
86
|
+
isInstructionOnly(project.tool)
|
|
87
|
+
? `Next: write the skill instructions in \`skills/${project.identity.name}/SKILL.md\`, then \`${cli} build\`.`
|
|
88
|
+
: `Next: write your operations in \`${ops}\`, then \`${cli} introspect && ${cli} build\`.`,
|
|
89
|
+
...(summary ? [summary] : []),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create dev.toolfactory/tool.json, the identity file and the binding's scaffold, build every
|
|
94
|
+
* selected surface, and leave a project that is already live in the harness it was run from:
|
|
95
|
+
* a git repository, the first-party skills, the rendered agent config, and — only when asked —
|
|
96
|
+
* a GitHub repository holding it.
|
|
97
|
+
*/
|
|
98
|
+
export function init(options) {
|
|
99
|
+
const root = resolve(options.root);
|
|
100
|
+
assertValidName(options.name);
|
|
101
|
+
mkdirSync(root, { recursive: true });
|
|
102
|
+
// First, because `.agents/setup` starts with `git rev-parse --show-toplevel` and fails outside
|
|
103
|
+
// a repository, and because `gh repo create --source` pushes one.
|
|
104
|
+
if (options.git !== false && !existsSync(join(root, ".git")))
|
|
105
|
+
git(root, ["init", "-q", "-b", "main"]);
|
|
106
|
+
const surfaces = [...new Set(options.surfaces ?? defaultSurfaces(options.binding))];
|
|
107
|
+
assertSurfaceRequirements(surfaces);
|
|
108
|
+
const usesBundle = surfaces.some((s) => BUNDLE_SURFACES.includes(s));
|
|
109
|
+
const identityPath = usesBundle
|
|
110
|
+
? "plugin.json"
|
|
111
|
+
: options.binding === "python"
|
|
112
|
+
? "pyproject.toml"
|
|
113
|
+
: "package.json";
|
|
114
|
+
const identity = compact({
|
|
115
|
+
name: options.name,
|
|
116
|
+
version: "0.1.0",
|
|
117
|
+
description: options.description,
|
|
118
|
+
license: options.license,
|
|
119
|
+
repository: options.repository,
|
|
120
|
+
author: options.author ? { name: options.author } : undefined,
|
|
121
|
+
keywords: options.keywords?.length ? options.keywords : [options.name],
|
|
122
|
+
});
|
|
123
|
+
const written = [];
|
|
124
|
+
if (usesBundle && !existsSync(join(root, "plugin.json"))) {
|
|
125
|
+
mkdirSync(root, { recursive: true });
|
|
126
|
+
writeFileSync(join(root, "plugin.json"), json({ $schema: PLUGIN_SCHEMA_ID, ...identity }));
|
|
127
|
+
written.push("plugin.json");
|
|
128
|
+
}
|
|
129
|
+
const tool = ToolConfigSchema.parse({
|
|
130
|
+
$schema: `./${TOOL_SCHEMA_PATH.split("/").pop()}`,
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
identity: identityPath,
|
|
133
|
+
binding: options.binding,
|
|
134
|
+
...(options.runtime ? { runtime: options.runtime } : {}),
|
|
135
|
+
surfaces,
|
|
136
|
+
});
|
|
137
|
+
const toolPath = join(root, TOOL_PATH);
|
|
138
|
+
if (!existsSync(toolPath)) {
|
|
139
|
+
mkdirSync(dirname(toolPath), { recursive: true });
|
|
140
|
+
writeFileSync(toolPath, json(tool));
|
|
141
|
+
written.push(TOOL_PATH);
|
|
142
|
+
}
|
|
143
|
+
const scaffoldProject = {
|
|
144
|
+
root,
|
|
145
|
+
tool,
|
|
146
|
+
identity,
|
|
147
|
+
identityExtra: {},
|
|
148
|
+
operations: [],
|
|
149
|
+
toolfactoryVersion: TOOLFACTORY_VERSION,
|
|
150
|
+
};
|
|
151
|
+
if (!isInstructionOnly(tool))
|
|
152
|
+
written.push(...writeIfAbsent(root, getBinding(options.binding).scaffold(scaffoldProject)));
|
|
153
|
+
// A region file that predates toolfactory has no markers, and `apply` refuses to guess where
|
|
154
|
+
// they go; appending the empty pair lets `init` run in a folder that already has these files.
|
|
155
|
+
for (const file of ["agents", "readme"].flatMap((id) => getSurface(id).plan(scaffoldProject))) {
|
|
156
|
+
if (file.kind !== "region")
|
|
157
|
+
continue;
|
|
158
|
+
const path = join(root, file.path);
|
|
159
|
+
if (!existsSync(path))
|
|
160
|
+
continue;
|
|
161
|
+
const text = readFileSync(path, "utf8");
|
|
162
|
+
const missing = file.regions.filter((region) => !text.includes(region.begin));
|
|
163
|
+
if (missing.length)
|
|
164
|
+
writeFileSync(path, `${text}\n${missing.map((region) => `${region.begin}\n${region.end}`).join("\n")}\n`);
|
|
165
|
+
}
|
|
166
|
+
// The kernel files exist from the first moment so `introspect` can spawn the server.
|
|
167
|
+
const built = build(root);
|
|
168
|
+
written.push(...built.result.written);
|
|
169
|
+
// Best-effort: the first-party skills for the selected surfaces, so an agent developing the
|
|
170
|
+
// tool from inside a harness that reads them has them from the first `init`.
|
|
171
|
+
const skills = [
|
|
172
|
+
surfaces.includes("web") ? "shadcn/ui@shadcn" : undefined,
|
|
173
|
+
surfaces.some((s) => s === "mcp" || s === "openclaw-native" || s === "hermes-native")
|
|
174
|
+
? "anthropics/skills@mcp-builder"
|
|
175
|
+
: undefined,
|
|
176
|
+
].filter((spec) => spec !== undefined);
|
|
177
|
+
for (const spec of skills) {
|
|
178
|
+
spawnSync("npx", ["--yes", "skills", "add", spec, "-y"], {
|
|
179
|
+
cwd: root,
|
|
180
|
+
stdio: "ignore",
|
|
181
|
+
timeout: 120_000,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const agentConfig = options.setup === false ? { setup: false, harnesses: [] } : agentSetup(root);
|
|
185
|
+
// A repository with no commit has nothing to push and no baseline to diff against; one that
|
|
186
|
+
// already has history is the author's, and init adds nothing to it. `--no-verify`: the
|
|
187
|
+
// pre-commit hook `.agents/setup` just installed converges agent config and re-runs the drift
|
|
188
|
+
// gate, which this build satisfied by construction, and requiring it here would make the first
|
|
189
|
+
// commit depend on a toolchain the project has not installed yet.
|
|
190
|
+
if (options.git !== false && !git(root, ["rev-parse", "--verify", "-q", "HEAD"])) {
|
|
191
|
+
if (git(root, ["add", "-A"])) {
|
|
192
|
+
git(root, [...commitIdentity(root), "commit", "-q", "--no-verify", "-m", "toolfactory init"]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const result = {
|
|
196
|
+
written,
|
|
197
|
+
agentConfig,
|
|
198
|
+
nextSteps: nextSteps(built.project, agentConfig),
|
|
199
|
+
};
|
|
200
|
+
if (options.repo) {
|
|
201
|
+
const project = loadProject(root);
|
|
202
|
+
const created = createRepo(project, {
|
|
203
|
+
slug: options.repo,
|
|
204
|
+
public: options.public,
|
|
205
|
+
dryRun: options.dryRun,
|
|
206
|
+
});
|
|
207
|
+
// Everything GitHub-only in one call: it creates the live environment only when the project
|
|
208
|
+
// has a live tier, and otherwise still sets the release secrets .env already holds.
|
|
209
|
+
const prepared = prepareRepository(project, {
|
|
210
|
+
repository: options.repo,
|
|
211
|
+
reviewers: options.reviewers,
|
|
212
|
+
dryRun: options.dryRun,
|
|
213
|
+
releaseSecrets: registries(project).flatMap((row) => row.secrets),
|
|
214
|
+
manual: manualSteps(project),
|
|
215
|
+
});
|
|
216
|
+
result.repository = {
|
|
217
|
+
...created,
|
|
218
|
+
commands: [...created.commands, ...prepared.commands],
|
|
219
|
+
secrets: prepared.secrets,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
export function build(root = ".") {
|
|
225
|
+
const project = loadProject(root);
|
|
226
|
+
return {
|
|
227
|
+
project,
|
|
228
|
+
result: {
|
|
229
|
+
...apply(project.root, buildPlan(project), TOOLFACTORY_VERSION),
|
|
230
|
+
nudge: unpublishNudge(project),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** Throws when any generated file drifted, so CLI and MCP callers see a failure. */
|
|
235
|
+
/** The drift gate: the operation snapshot and every generated file must match the code. */
|
|
236
|
+
export async function check(root = ".") {
|
|
237
|
+
const project = loadProject(root);
|
|
238
|
+
assertNoSensitiveArgument(project.tool.config, project.operations);
|
|
239
|
+
const drift = checkPlan(project.root, buildPlan(project), TOOLFACTORY_VERSION);
|
|
240
|
+
const ops = isInstructionOnly(project.tool) ? undefined : await snapshot(project);
|
|
241
|
+
if (ops?.changed)
|
|
242
|
+
drift.unshift({ kind: "changed", path: OPS_PATH });
|
|
243
|
+
if (drift.length) {
|
|
244
|
+
throw new Error(`${drift.length} generated file(s) out of date; run \`toolfactory ${ops?.changed ? "introspect" : "build"}\`:\n${drift.map((d) => ` ${d.kind}: ${d.path}`).join("\n")}`);
|
|
245
|
+
}
|
|
246
|
+
return { project, drift };
|
|
247
|
+
}
|
|
248
|
+
/** Regenerate the kernel first so the snapshot always reflects the current templates. */
|
|
249
|
+
export async function introspect(root = ".") {
|
|
250
|
+
const { project } = build(root);
|
|
251
|
+
if (isInstructionOnly(project.tool))
|
|
252
|
+
throw new Error('runtime "none" has no kernel to introspect; edit SKILL.md and run `toolfactory build`.');
|
|
253
|
+
const snapshot = await runIntrospect(project);
|
|
254
|
+
// Against the operations the kernel just reported, not the ones on disk: this is the moment an
|
|
255
|
+
// author who routed a secret through an argument finds out, before it reaches any surface.
|
|
256
|
+
assertNoSensitiveArgument(project.tool.config, snapshot.ops.tools.map(toOperation));
|
|
257
|
+
if (snapshot.changed)
|
|
258
|
+
build(root);
|
|
259
|
+
return snapshot;
|
|
260
|
+
}
|
|
261
|
+
export function coverage(root = ".") {
|
|
262
|
+
const project = loadProject(root);
|
|
263
|
+
return computeCoverage(project, selectedSurfaces(project.tool.surfaces));
|
|
264
|
+
}
|
|
265
|
+
/** Run every selected surface's upstream validator (tier 1 and the kernel smoke); throws if any fails. */
|
|
266
|
+
export function validate(root = ".", only) {
|
|
267
|
+
const project = loadProject(root);
|
|
268
|
+
const surfaces = selectedSurfaces(project.tool.surfaces).filter((s) => !only || s.id === only);
|
|
269
|
+
const outcomes = [];
|
|
270
|
+
for (const surface of surfaces) {
|
|
271
|
+
if (surface.id === "agent-plugins") {
|
|
272
|
+
const problems = validateAgentPlugin(project.root);
|
|
273
|
+
outcomes.push({
|
|
274
|
+
label: "agent-plugins schema",
|
|
275
|
+
command: "ajv (vendored Agent Plugins 1.0.0 schemas)",
|
|
276
|
+
ok: problems.length === 0,
|
|
277
|
+
output: problems.map((p) => `${p.file}: ${p.message}`).join("\n"),
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
for (const command of surface.validate?.(project) ?? []) {
|
|
282
|
+
outcomes.push(run(command));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const failed = outcomes.filter((outcome) => !outcome.ok);
|
|
286
|
+
if (failed.length) {
|
|
287
|
+
throw new Error(`${failed.length} validator(s) failed:\n${failed
|
|
288
|
+
.map((f) => ` ${f.label}: ${f.command}\n${f.output
|
|
289
|
+
.split("\n")
|
|
290
|
+
.map((l) => ` ${l}`)
|
|
291
|
+
.join("\n")}`)
|
|
292
|
+
.join("\n")}`);
|
|
293
|
+
}
|
|
294
|
+
return outcomes;
|
|
295
|
+
}
|
|
296
|
+
export function run(command) {
|
|
297
|
+
const result = spawnSync(command.command, command.args, {
|
|
298
|
+
cwd: command.cwd,
|
|
299
|
+
env: { ...process.env, ...command.env },
|
|
300
|
+
encoding: "utf8",
|
|
301
|
+
timeout: 300_000,
|
|
302
|
+
});
|
|
303
|
+
const output = [result.stdout, result.stderr, result.error?.message]
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
.join("\n")
|
|
306
|
+
.trim();
|
|
307
|
+
return {
|
|
308
|
+
label: command.label,
|
|
309
|
+
command: [command.command, ...command.args].join(" "),
|
|
310
|
+
ok: result.status === 0,
|
|
311
|
+
output,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Run a step list from `project/gate.ts` in order, stopping at the first failure. `when: "ci"`
|
|
316
|
+
* steps are runner provisioning — a global install no checkout may perform on the author's
|
|
317
|
+
* machine — and are skipped here exactly as `renderShell` skips them. Every step's output goes to
|
|
318
|
+
* this process's stderr, never its stdout, so running the gate over the MCP surface cannot
|
|
319
|
+
* corrupt the protocol stream.
|
|
320
|
+
*/
|
|
321
|
+
export function runSteps(root, steps) {
|
|
322
|
+
const cwd = resolve(root);
|
|
323
|
+
const results = [];
|
|
324
|
+
for (const step of steps) {
|
|
325
|
+
if (step.when === "ci")
|
|
326
|
+
continue;
|
|
327
|
+
const started = Date.now();
|
|
328
|
+
const { status } = spawnSync(step.run, {
|
|
329
|
+
cwd,
|
|
330
|
+
shell: true,
|
|
331
|
+
stdio: ["ignore", 2, 2],
|
|
332
|
+
env: { ...process.env, ...step.env },
|
|
333
|
+
timeout: 1_800_000,
|
|
334
|
+
});
|
|
335
|
+
results.push({ name: step.name, ok: status === 0, durationMs: Date.now() - started });
|
|
336
|
+
if (status !== 0)
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
return { steps: results };
|
|
340
|
+
}
|
|
341
|
+
function green(label, result) {
|
|
342
|
+
const failed = result.steps.find((step) => !step.ok);
|
|
343
|
+
if (failed) {
|
|
344
|
+
throw new Error(`${label} failed at step "${failed.name}" after ${failed.durationMs} ms; its output is above.`);
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The gate: what "green" means for this project, run here instead of on a runner. `ci.yml`
|
|
350
|
+
* renders the same list, so a project with no CI at all — no GitHub, plain git or none — has the
|
|
351
|
+
* identical gate one command away.
|
|
352
|
+
*/
|
|
353
|
+
export function gate(root = ".") {
|
|
354
|
+
return green("gate", runSteps(root, gateSteps(loadProject(root))));
|
|
355
|
+
}
|
|
356
|
+
/** Every release asset into `dist/release/`, by the same steps the release workflow's job runs. */
|
|
357
|
+
export function packageRelease(root = ".") {
|
|
358
|
+
return green("package", runSteps(root, packageSteps(loadProject(root))));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Where the end user of the built tool types a config value, per selected surface: the same text
|
|
362
|
+
* the README's Configuration lines and the skill's operations block carry, so an agent inside a
|
|
363
|
+
* harness can read it without calling anything. No runtime host detection — the selection is the
|
|
364
|
+
* only input, because that is what decides which hosts exist at all.
|
|
365
|
+
*/
|
|
366
|
+
const CONFIG_HOSTS = {
|
|
367
|
+
claude: () => "Claude Code: `/plugin` -> this plugin -> configure. Masked input, stored in the OS keychain.",
|
|
368
|
+
mcpb: () => "Claude Desktop: the masked field in the `.mcpb` install dialog.",
|
|
369
|
+
gemini: (project, name) => `Gemini CLI: \`gemini extensions config ${projectName.gemini(project.identity.name)} ${name}\`.`,
|
|
370
|
+
"openclaw-native": (project) => `OpenClaw: the Control UI at http://localhost:18789 -> Plugins -> ${project.identity.name}; \`uiHints.sensitive\` masks the field.`,
|
|
371
|
+
"hermes-native": () => "Hermes: the env file `hermes config env-path` prints.",
|
|
372
|
+
cursor: (_project, name) => `Cursor: \`\${env:${name}}\` in the MCP entry, with the value exported in the environment.`,
|
|
373
|
+
codex: (_project, name) => `Codex: the environment only — upstream has no plugin secret path (openai/codex#24401), so export ${name} before running \`codex\`.`,
|
|
374
|
+
};
|
|
375
|
+
/** `<root>/.env`, parsed for presence only. Missing or unreadable is simply "nothing set here". */
|
|
376
|
+
function envValues(root) {
|
|
377
|
+
const path = join(root, ".env");
|
|
378
|
+
if (!existsSync(path))
|
|
379
|
+
return {};
|
|
380
|
+
try {
|
|
381
|
+
return parseEnv(readFileSync(path, "utf8"));
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return {};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Secret *names* GitHub already holds, from `gh secret list`; undefined when `gh` cannot answer. */
|
|
388
|
+
function githubSecretNames(project) {
|
|
389
|
+
const slug = githubSlug(project.identity.repository);
|
|
390
|
+
if (!slug)
|
|
391
|
+
return undefined;
|
|
392
|
+
const names = new Set();
|
|
393
|
+
let answered = false;
|
|
394
|
+
for (const scope of [[], ["--env", LIVE_ENVIRONMENT]]) {
|
|
395
|
+
const result = spawnSync("gh", ["secret", "list", "--repo", slug, ...scope], {
|
|
396
|
+
encoding: "utf8",
|
|
397
|
+
timeout: 30_000,
|
|
398
|
+
});
|
|
399
|
+
if (result.status !== 0)
|
|
400
|
+
continue;
|
|
401
|
+
answered = true;
|
|
402
|
+
for (const line of result.stdout.split("\n")) {
|
|
403
|
+
const name = line.trim().split(/\s+/)[0];
|
|
404
|
+
if (name)
|
|
405
|
+
names.add(name);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return answered ? names : undefined;
|
|
409
|
+
}
|
|
410
|
+
/** The `wxt submit --dry-run` probes read the zips `toolfactory package` writes; without them, say so. */
|
|
411
|
+
function storeZipsPresent(project) {
|
|
412
|
+
return existsSync(join(project.root, BROWSER_HOST_DIR, ".output"));
|
|
413
|
+
}
|
|
414
|
+
function releaseHowTo(project, row, name) {
|
|
415
|
+
const lines = [
|
|
416
|
+
`Mint it at ${row.url}.`,
|
|
417
|
+
`Add \`${name}=\` to .env, then \`${toolfactoryCli(project)} bootstrap-repo\` pushes it as a repository secret.`,
|
|
418
|
+
`Used by the release's \`${releaseJob(row)}\` leg.`,
|
|
419
|
+
];
|
|
420
|
+
if (!row.probe) {
|
|
421
|
+
lines.push(`No read-only endpoint exists for ${row.id}, so \`secrets check\` cannot verify this one; the release's own submit is the check.`);
|
|
422
|
+
}
|
|
423
|
+
return lines;
|
|
424
|
+
}
|
|
425
|
+
function configHowTo(project, name) {
|
|
426
|
+
const lines = [
|
|
427
|
+
`Add \`${name}=\` to .env — gitignored, and what the live tests and \`${toolfactoryCli(project)} bootstrap-repo\` read.`,
|
|
428
|
+
];
|
|
429
|
+
if (has(project, "web")) {
|
|
430
|
+
lines.push(`Or open the Secrets panel in a browser: \`${project.identity.name} mcp --http --open\`.`);
|
|
431
|
+
}
|
|
432
|
+
for (const [surface, line] of Object.entries(CONFIG_HOSTS)) {
|
|
433
|
+
if (has(project, surface))
|
|
434
|
+
lines.push(line(project, name));
|
|
435
|
+
}
|
|
436
|
+
return lines;
|
|
437
|
+
}
|
|
438
|
+
/** The live test is the check for config keys: it is already gated on exactly these credentials. */
|
|
439
|
+
function liveTestCommand(project) {
|
|
440
|
+
return project.tool.binding === "python"
|
|
441
|
+
? LIVE_TEST_COMMAND
|
|
442
|
+
: PACKAGE_MANAGER_COMMANDS[project.packageManager ?? "npm"].run("test:live");
|
|
443
|
+
}
|
|
444
|
+
/** The release.yml job a registry row's leg lives in. */
|
|
445
|
+
function releaseJob(row) {
|
|
446
|
+
if (["chrome", "firefox", "edge"].includes(row.id))
|
|
447
|
+
return "publish-browser-ext";
|
|
448
|
+
if (row.id === "safari")
|
|
449
|
+
return "publish-browser-ext-safari";
|
|
450
|
+
if (row.id === "pages")
|
|
451
|
+
return "pages-deploy";
|
|
452
|
+
return `publish-${row.id === "clawhub-package" ? "clawhub" : row.id}`;
|
|
453
|
+
}
|
|
454
|
+
export function secretsReport(project, action = "status", key) {
|
|
455
|
+
const values = { ...envValues(project.root) };
|
|
456
|
+
const properties = configProperties(project);
|
|
457
|
+
const required = new Set(requiredConfig(project));
|
|
458
|
+
const github = githubSecretNames(project);
|
|
459
|
+
const rows = [];
|
|
460
|
+
for (const [configKey, property] of Object.entries(properties)) {
|
|
461
|
+
if (!isSensitive(property))
|
|
462
|
+
continue;
|
|
463
|
+
const name = envName(configKey);
|
|
464
|
+
const meta = (property["x-toolfactory"] ?? {});
|
|
465
|
+
rows.push({
|
|
466
|
+
name,
|
|
467
|
+
kind: "config",
|
|
468
|
+
required: required.has(configKey),
|
|
469
|
+
needs: Object.keys(CONFIG_HOSTS).filter((surface) => has(project, surface)),
|
|
470
|
+
local: Boolean(values[name] ?? process.env[name]),
|
|
471
|
+
...(github ? { github: github.has(name) } : {}),
|
|
472
|
+
...(meta.url ? { url: meta.url } : {}),
|
|
473
|
+
howTo: configHowTo(project, name),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const byName = new Map();
|
|
477
|
+
for (const row of registries(project)) {
|
|
478
|
+
for (const name of row.secrets)
|
|
479
|
+
byName.set(name, [...(byName.get(name) ?? []), row]);
|
|
480
|
+
}
|
|
481
|
+
for (const [name, owners] of byName) {
|
|
482
|
+
const first = owners[0];
|
|
483
|
+
rows.push({
|
|
484
|
+
name,
|
|
485
|
+
kind: "release",
|
|
486
|
+
required: false,
|
|
487
|
+
needs: owners.map((row) => row.id),
|
|
488
|
+
local: Boolean(values[name] ?? process.env[name]),
|
|
489
|
+
...(github ? { github: github.has(name) } : {}),
|
|
490
|
+
url: first.url,
|
|
491
|
+
howTo: releaseHowTo(project, first, name),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const selected = key ? rows.filter((row) => row.name === key) : rows;
|
|
495
|
+
if (action === "check")
|
|
496
|
+
checkSecrets(project, selected, values);
|
|
497
|
+
return { secrets: selected, manual: manualSteps(project) };
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Delegation, never re-implemented auth: each release row runs the one upstream command its
|
|
501
|
+
* registry answers with, and config keys are checked by the T4 live test, which is already gated
|
|
502
|
+
* on exactly them and is already the author's own proof that the credential works.
|
|
503
|
+
*/
|
|
504
|
+
function checkSecrets(project, rows, values) {
|
|
505
|
+
const present = (name) => Boolean(values[name] ?? process.env[name]);
|
|
506
|
+
const table = registries(project);
|
|
507
|
+
for (const row of rows) {
|
|
508
|
+
if (row.kind !== "release" || !row.local)
|
|
509
|
+
continue;
|
|
510
|
+
const registry = table.find((entry) => entry.id === row.needs[0]);
|
|
511
|
+
if (!registry?.probe || !registry.secrets.every(present))
|
|
512
|
+
continue;
|
|
513
|
+
if (registry.surfaces.includes("browser-extension") && !storeZipsPresent(project)) {
|
|
514
|
+
row.howTo.push(`\`${toolfactoryCli(project)} package\` first: the store dry run submits the zips it writes.`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const outcome = run({
|
|
518
|
+
label: registry.id,
|
|
519
|
+
command: "bash",
|
|
520
|
+
args: ["-c", registry.probe],
|
|
521
|
+
cwd: project.root,
|
|
522
|
+
env: values,
|
|
523
|
+
});
|
|
524
|
+
row.check = { ok: outcome.ok, command: registry.probe };
|
|
525
|
+
}
|
|
526
|
+
const live = liveCredentials(project);
|
|
527
|
+
if (!live.length || !live.map(envName).every(present))
|
|
528
|
+
return;
|
|
529
|
+
const command = liveTestCommand(project);
|
|
530
|
+
const outcome = run({
|
|
531
|
+
label: "live tests",
|
|
532
|
+
command: "bash",
|
|
533
|
+
args: ["-c", command],
|
|
534
|
+
cwd: project.root,
|
|
535
|
+
env: values,
|
|
536
|
+
});
|
|
537
|
+
for (const row of rows) {
|
|
538
|
+
if (row.kind === "config")
|
|
539
|
+
row.check = { ok: outcome.ok, command };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** The whole inventory: what each credential is for, where it is set, and (check) whether it works. */
|
|
543
|
+
export function secrets(args) {
|
|
544
|
+
return secretsReport(loadProject(args.root ?? "."), args.action ?? "status", args.key);
|
|
545
|
+
}
|
|
546
|
+
/** One line for `init`'s next steps; nothing at all when the project declares no credential. */
|
|
547
|
+
function secretsSummary(report) {
|
|
548
|
+
const { secrets: rows } = report;
|
|
549
|
+
if (!rows.length)
|
|
550
|
+
return undefined;
|
|
551
|
+
const missing = rows.filter((row) => !row.local).map((row) => row.name);
|
|
552
|
+
return `Secrets: ${rows.length - missing.length} of ${rows.length} present in .env${missing.length ? ` (missing ${missing.join(", ")})` : ""}. \`toolfactory secrets\` prints where each one is set, per host and per registry.`;
|
|
553
|
+
}
|
|
554
|
+
/** The project as the previous tag left it, plus the surfaces it published that are now gone. */
|
|
555
|
+
function retractable(project, tag, tool) {
|
|
556
|
+
const previous = {
|
|
557
|
+
...project,
|
|
558
|
+
tool,
|
|
559
|
+
// The gate asserts `v<version>` against the tag, so the tag *is* the version that tag published.
|
|
560
|
+
identity: { ...project.identity, version: tag.replace(/^v/, "") },
|
|
561
|
+
};
|
|
562
|
+
const published = new Set(registries(previous).flatMap((row) => row.surfaces));
|
|
563
|
+
return {
|
|
564
|
+
previous,
|
|
565
|
+
dropped: droppedSurfaces(tool.surfaces, project.tool.surfaces).filter((surface) => published.has(surface)),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
export function unpublish(root = ".", options = {}) {
|
|
569
|
+
const project = loadProject(root);
|
|
570
|
+
const from = previousTag(project.root, options.ref ?? "HEAD");
|
|
571
|
+
const tool = from ? toolAtRef(project.root, from) : undefined;
|
|
572
|
+
if (!from || !tool)
|
|
573
|
+
return { from, dropped: [], steps: [] };
|
|
574
|
+
const { previous, dropped } = retractable(project, from, tool);
|
|
575
|
+
const steps = unpublishSteps(previous, dropped, options.hard);
|
|
576
|
+
const listed = steps.map(({ name, run }) => ({ name, run }));
|
|
577
|
+
if (options.dryRun)
|
|
578
|
+
return { from, dropped, steps: listed };
|
|
579
|
+
return { from, dropped, steps: listed, ran: runSteps(project.root, steps).steps };
|
|
580
|
+
}
|
|
581
|
+
/** `build`'s one line: a surface that published somewhere is gone, and nothing has retracted it yet. */
|
|
582
|
+
function unpublishNudge(project) {
|
|
583
|
+
const from = previousTag(project.root);
|
|
584
|
+
const tool = from ? toolAtRef(project.root, from) : undefined;
|
|
585
|
+
if (!from || !tool)
|
|
586
|
+
return undefined;
|
|
587
|
+
const { dropped } = retractable(project, from, tool);
|
|
588
|
+
if (!dropped.length)
|
|
589
|
+
return undefined;
|
|
590
|
+
return `${dropped.join(", ")} published at ${from} and is no longer selected: \`${toolfactoryCli(project)} unpublish --dry-run\` shows what the next release will retract.`;
|
|
591
|
+
}
|
|
592
|
+
export function adopt(root, path) {
|
|
593
|
+
setState(resolve(root), path, "manual", TOOLFACTORY_VERSION);
|
|
594
|
+
build(root);
|
|
595
|
+
}
|
|
596
|
+
export function unadopt(root, path) {
|
|
597
|
+
setState(resolve(root), path, "generated", TOOLFACTORY_VERSION);
|
|
598
|
+
build(root);
|
|
599
|
+
}
|
|
600
|
+
export function eject(root, surface) {
|
|
601
|
+
const project = loadProject(root);
|
|
602
|
+
const owned = buildPlan(project, selectedSurfaces([surface])).map((file) => file.path);
|
|
603
|
+
const lock = readLock(project.root);
|
|
604
|
+
const adopted = [];
|
|
605
|
+
for (const path of owned) {
|
|
606
|
+
if (lock?.files[path]) {
|
|
607
|
+
setState(project.root, path, "manual", TOOLFACTORY_VERSION);
|
|
608
|
+
adopted.push(path);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
build(root);
|
|
612
|
+
return adopted;
|
|
613
|
+
}
|
|
614
|
+
/** Report the upstream CLIs this machine can delegate to. */
|
|
615
|
+
/**
|
|
616
|
+
* Prepare the GitHub repository: the `live-tests` environment and its secrets, the release
|
|
617
|
+
* registries' repository secrets, Pages, and npm's trusted publisher — everything GitHub only
|
|
618
|
+
* exposes over its API, from the one table and the one `.env`.
|
|
619
|
+
*/
|
|
620
|
+
export function bootstrapRepo(args) {
|
|
621
|
+
const project = loadProject(args.root);
|
|
622
|
+
return prepareRepository(project, {
|
|
623
|
+
reviewers: args.reviewers,
|
|
624
|
+
dryRun: args.dryRun,
|
|
625
|
+
releaseSecrets: registries(project).flatMap((row) => row.secrets),
|
|
626
|
+
manual: manualSteps(project),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
export function doctor() {
|
|
630
|
+
const probes = {
|
|
631
|
+
git: ["git", ["--version"]],
|
|
632
|
+
gh: ["gh", ["--version"]],
|
|
633
|
+
npm: ["npm", ["--version"]],
|
|
634
|
+
uv: ["uv", ["--version"]],
|
|
635
|
+
claude: ["claude", ["--version"]],
|
|
636
|
+
openclaw: ["openclaw", ["--version"]],
|
|
637
|
+
clawhub: ["clawhub", ["--version"]],
|
|
638
|
+
hermes: ["hermes", ["--version"]],
|
|
639
|
+
uvx: ["uvx", ["--version"]],
|
|
640
|
+
agentskills: ["uvx", ["--from", "skills-ref", "agentskills", "--help"]],
|
|
641
|
+
docker: ["docker", ["--version"]],
|
|
642
|
+
};
|
|
643
|
+
const tools = {};
|
|
644
|
+
// Never spawn npx from here: under an npx-launched MCP host that deadlocks on npm's cache lock.
|
|
645
|
+
try {
|
|
646
|
+
const inspector = createRequire(import.meta.url)("@modelcontextprotocol/inspector/package.json");
|
|
647
|
+
tools["mcp-inspector"] = `${inspector.version} (npx @modelcontextprotocol/inspector)`;
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
tools["mcp-inspector"] =
|
|
651
|
+
"missing (npx --yes @modelcontextprotocol/inspector installs it on demand)";
|
|
652
|
+
}
|
|
653
|
+
for (const [name, [command, args]] of Object.entries(probes)) {
|
|
654
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout: 10_000 });
|
|
655
|
+
tools[name] =
|
|
656
|
+
result.status === 0
|
|
657
|
+
? ((result.stdout || result.stderr).trim().split("\n")[0] ?? "ok")
|
|
658
|
+
: "missing";
|
|
659
|
+
}
|
|
660
|
+
return { toolfactory: TOOLFACTORY_VERSION, node: process.version, tools };
|
|
661
|
+
}
|
|
662
|
+
export const SURFACES = SURFACE_IDS;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|