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,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything toolfactory does to a GitHub repository: creating and pushing it (`init --repo`),
|
|
3
|
+
* and preparing the T4 live tier — an environment with required reviewers and the secrets inside
|
|
4
|
+
* it, the two things GitHub only exposes over its API. All of it through the official `gh` CLI
|
|
5
|
+
* (§8 C1: own no client), never a hand-rolled HTTP call, and all of it optional: a project with
|
|
6
|
+
* no GitHub at all never reaches this file.
|
|
7
|
+
*
|
|
8
|
+
* Secret values are read from the local `.env`, piped to `gh` on stdin, and never written to a
|
|
9
|
+
* command line, a log line, or the returned result.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { parseEnv } from "node:util";
|
|
15
|
+
import { envName, has, liveCredentials, npmName } from "../surfaces/shared.js";
|
|
16
|
+
/** The GitHub environment the generated `live` job deploys to. */
|
|
17
|
+
export const LIVE_ENVIRONMENT = "live-tests";
|
|
18
|
+
/** `owner/repo` from a GitHub URL or `git@` remote. */
|
|
19
|
+
export function githubSlug(repository) {
|
|
20
|
+
const match = repository?.match(/github\.com[/:]([^/]+)\/([^/#?]+?)(?:\.git)?(?:[/#?]|$)/);
|
|
21
|
+
return match ? `${match[1]}/${match[2]}` : undefined;
|
|
22
|
+
}
|
|
23
|
+
function gh(args, stdin, cwd) {
|
|
24
|
+
return execFileSync("gh", args, {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
input: stdin,
|
|
28
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
29
|
+
}).trim();
|
|
30
|
+
}
|
|
31
|
+
/** npm trust's direct-publish permission flags landed in npm 11.15.0. */
|
|
32
|
+
function supportsNpmTrust() {
|
|
33
|
+
const result = spawnSync("npm", ["--version"], { encoding: "utf8", timeout: 10_000 });
|
|
34
|
+
const version = result.status === 0 ? result.stdout.trim() : "unavailable";
|
|
35
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
36
|
+
return { ok: major > 11 || (major === 11 && minor >= 15), version };
|
|
37
|
+
}
|
|
38
|
+
export function bootstrapRepo(project, options = {}) {
|
|
39
|
+
const repository = options.repository ?? githubSlug(project.identity.repository);
|
|
40
|
+
if (!repository) {
|
|
41
|
+
throw new Error("bootstrap-repo needs a GitHub repository URL in the identity file to know which repo to prepare.");
|
|
42
|
+
}
|
|
43
|
+
const { reviewers = [], dryRun = false, releaseSecrets = [] } = options;
|
|
44
|
+
// npm setup below reports its actual result, replacing the generic status instructions.
|
|
45
|
+
const manual = (options.manual ?? []).filter((step) => !step.startsWith("npm:"));
|
|
46
|
+
const envFile = options.envFile ?? join(project.root, ".env");
|
|
47
|
+
const values = existsSync(envFile) ? parseEnv(readFileSync(envFile, "utf8")) : {};
|
|
48
|
+
const keys = liveCredentials(project);
|
|
49
|
+
const commands = [];
|
|
50
|
+
const written = [];
|
|
51
|
+
const missing = [];
|
|
52
|
+
// The live tier's own environment, and only when the project has a live tier: a `tool.json`
|
|
53
|
+
// config key that is both required and sensitive is the whole trigger (§6 T4).
|
|
54
|
+
if (keys.length) {
|
|
55
|
+
const ids = reviewers.map((login) => {
|
|
56
|
+
const args = ["api", `users/${login}`, "--jq", ".id"];
|
|
57
|
+
commands.push(`gh ${args.join(" ")}`);
|
|
58
|
+
return dryRun ? `<id of ${login}>` : gh(args);
|
|
59
|
+
});
|
|
60
|
+
const body = JSON.stringify(reviewers.length
|
|
61
|
+
? { reviewers: ids.map((id) => ({ type: "User", id: Number(id) || id })) }
|
|
62
|
+
: {});
|
|
63
|
+
const environmentArgs = [
|
|
64
|
+
"api",
|
|
65
|
+
"--method",
|
|
66
|
+
"PUT",
|
|
67
|
+
`repos/${repository}/environments/${LIVE_ENVIRONMENT}`,
|
|
68
|
+
"--input",
|
|
69
|
+
"-",
|
|
70
|
+
];
|
|
71
|
+
commands.push(`gh ${environmentArgs.join(" ")} <<< '${body}'`);
|
|
72
|
+
if (!dryRun)
|
|
73
|
+
gh(environmentArgs, body);
|
|
74
|
+
}
|
|
75
|
+
// Two scopes, one loop: a config key is the live job's, a registry token is the release's, and
|
|
76
|
+
// one `gh secret set -f .env` could not tell them apart. The value goes in on stdin either way,
|
|
77
|
+
// so it never reaches argv (`ps`), a log line, or this result.
|
|
78
|
+
const scoped = [
|
|
79
|
+
...keys.map((key) => [envName(key), ["--env", LIVE_ENVIRONMENT]]),
|
|
80
|
+
...[...new Set(releaseSecrets)].map((name) => [name, []]),
|
|
81
|
+
];
|
|
82
|
+
for (const [name, scope] of scoped) {
|
|
83
|
+
if (!dryRun && !values[name]) {
|
|
84
|
+
missing.push(name);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const args = ["secret", "set", name, ...scope, "--repo", repository];
|
|
88
|
+
commands.push(`gh ${args.join(" ")} # value from ${envFile}, on stdin`);
|
|
89
|
+
if (!dryRun)
|
|
90
|
+
gh(args, values[name]);
|
|
91
|
+
written.push(name);
|
|
92
|
+
}
|
|
93
|
+
// Pages: a workflow's own token cannot enable it (`GITHUB_TOKEN` has no `administration` key at
|
|
94
|
+
// all), but a local `gh` session is a repository admin, which is exactly the bar. Idempotent:
|
|
95
|
+
// 404 from the read means "not enabled yet", anything else means it already is.
|
|
96
|
+
if (has(project, "web")) {
|
|
97
|
+
const read = ["api", `repos/${repository}/pages`];
|
|
98
|
+
const create = [
|
|
99
|
+
"api",
|
|
100
|
+
"--method",
|
|
101
|
+
"POST",
|
|
102
|
+
`repos/${repository}/pages`,
|
|
103
|
+
"-f",
|
|
104
|
+
"build_type=workflow",
|
|
105
|
+
];
|
|
106
|
+
commands.push(`gh ${read.join(" ")} || gh ${create.join(" ")}`);
|
|
107
|
+
if (!dryRun && !try_(() => gh(read)))
|
|
108
|
+
try_(() => gh(create));
|
|
109
|
+
}
|
|
110
|
+
// npm trusted publishing, so the release can use OIDC — but only once the package exists.
|
|
111
|
+
// npm trust requires an interactive, logged-in 2FA session; granular bypass-2FA tokens are
|
|
112
|
+
// explicitly unsupported, so never smuggle NPM_TOKEN into this command.
|
|
113
|
+
if (releaseSecrets.includes("NPM_TOKEN")) {
|
|
114
|
+
const pkg = npmName(project);
|
|
115
|
+
const trustArgs = [
|
|
116
|
+
"trust",
|
|
117
|
+
"github",
|
|
118
|
+
pkg,
|
|
119
|
+
"--file",
|
|
120
|
+
"release.yml",
|
|
121
|
+
"--repo",
|
|
122
|
+
repository,
|
|
123
|
+
"--allow-publish",
|
|
124
|
+
"-y",
|
|
125
|
+
];
|
|
126
|
+
const trust = `npm ${trustArgs.join(" ")}`;
|
|
127
|
+
const confirmArgs = [
|
|
128
|
+
"variable",
|
|
129
|
+
"set",
|
|
130
|
+
"NPM_TRUSTED_PUBLISHER",
|
|
131
|
+
"-b",
|
|
132
|
+
"true",
|
|
133
|
+
"--repo",
|
|
134
|
+
repository,
|
|
135
|
+
];
|
|
136
|
+
const confirm = `gh ${confirmArgs.join(" ")}`;
|
|
137
|
+
const manualTrust = `npm: \`${trust}\` requires npm@11.15+, a package that already exists, and a logged-in 2FA session; granular access tokens with bypass 2FA are unsupported. Run it from that session, then \`${confirm}\`. Until it succeeds, NPM_TOKEN is the release fallback.`;
|
|
138
|
+
const published = !dryRun &&
|
|
139
|
+
spawnSync("npm", ["view", pkg, "version"], { stdio: "ignore", timeout: 60_000 }).status === 0;
|
|
140
|
+
if (!published) {
|
|
141
|
+
manual.push(`npm: the first publish uses NPM_TOKEN because \`npm trust\` requires the package to exist. Afterwards ${manualTrust.slice("npm: ".length)}`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const npm = supportsNpmTrust();
|
|
145
|
+
if (!npm.ok) {
|
|
146
|
+
manual.push(`npm: did not run \`${trust}\` because npm ${npm.version} lacks npm@11.15+'s --allow-publish permission. Install npm@^11.15 in the logged-in 2FA session, then ${manualTrust.slice("npm: ".length)}`);
|
|
147
|
+
}
|
|
148
|
+
else if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
149
|
+
manual.push(`npm: run bootstrap-repo from an interactive terminal for npm's 2FA challenge. ${manualTrust.slice("npm: ".length)}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
commands.push(trust);
|
|
153
|
+
const result = spawnSync("npm", trustArgs, { stdio: "inherit", timeout: 120_000 });
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
manual.push(`npm: \`${trust}\` exited ${result.status}. ${manualTrust.slice("npm: ".length)}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
commands.push(confirm);
|
|
159
|
+
try {
|
|
160
|
+
gh(confirmArgs);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
manual.push(`npm: trusted publishing was configured, but \`${confirm}\` did not succeed. Run it before relying on OIDC; NPM_TOKEN remains the release fallback until then.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
repository,
|
|
171
|
+
environment: LIVE_ENVIRONMENT,
|
|
172
|
+
reviewers,
|
|
173
|
+
secrets: written,
|
|
174
|
+
missing,
|
|
175
|
+
commands,
|
|
176
|
+
manual,
|
|
177
|
+
dryRun,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** `gh` exits non-zero for "not found" as readily as for a real failure; both mean "keep going". */
|
|
181
|
+
function try_(call) {
|
|
182
|
+
try {
|
|
183
|
+
call();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* One GitHub topic per selected surface, so a repository `init` creates is discoverable as what
|
|
192
|
+
* it actually ships. Surfaces that only package the same tool (`cli`, `npm`, `pypi`, `web`,
|
|
193
|
+
* `clawhub`, `workflows`) name no ecosystem and get none.
|
|
194
|
+
*/
|
|
195
|
+
export const SURFACE_TOPICS = {
|
|
196
|
+
skill: "agent-skill",
|
|
197
|
+
"agent-plugins": "agent-plugins",
|
|
198
|
+
claude: "claude-plugin",
|
|
199
|
+
codex: "codex-plugin",
|
|
200
|
+
cursor: "cursor-plugin",
|
|
201
|
+
mcp: "mcp-server",
|
|
202
|
+
"mcp-registry": "mcp-registry",
|
|
203
|
+
"openclaw-native": "openclaw-plugin",
|
|
204
|
+
"hermes-native": "hermes-plugin",
|
|
205
|
+
dsh: "dsh-plugin",
|
|
206
|
+
};
|
|
207
|
+
/** `gh <args>`, copy-pasteable: only arguments that need quoting get it. */
|
|
208
|
+
function render(args) {
|
|
209
|
+
return `gh ${args.map((arg) => (/[\s"'$]/.test(arg) ? JSON.stringify(arg) : arg)).join(" ")}`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Create the repository from the checkout and push it: `gh repo create --source --remote --push`,
|
|
213
|
+
* private unless asked otherwise, then one `gh repo edit --add-topic` for the selected surfaces.
|
|
214
|
+
* `gh` is the only GitHub client (§8 C1); nothing here talks to the API directly.
|
|
215
|
+
*/
|
|
216
|
+
export function createRepo(project, options) {
|
|
217
|
+
const { slug, dryRun = false } = options;
|
|
218
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(slug)) {
|
|
219
|
+
throw new Error(`repo takes owner/name, not ${JSON.stringify(slug)}.`);
|
|
220
|
+
}
|
|
221
|
+
const visibility = options.public ? "public" : "private";
|
|
222
|
+
const topics = [
|
|
223
|
+
...new Set(project.tool.surfaces
|
|
224
|
+
.flatMap((surface) => SURFACE_TOPICS[surface] ?? [])
|
|
225
|
+
.concat("toolfactory")),
|
|
226
|
+
];
|
|
227
|
+
const commands = [];
|
|
228
|
+
const create = [
|
|
229
|
+
"repo",
|
|
230
|
+
"create",
|
|
231
|
+
slug,
|
|
232
|
+
`--${visibility}`,
|
|
233
|
+
...(project.identity.description ? ["--description", project.identity.description] : []),
|
|
234
|
+
"--source",
|
|
235
|
+
".",
|
|
236
|
+
"--remote",
|
|
237
|
+
"origin",
|
|
238
|
+
"--push",
|
|
239
|
+
];
|
|
240
|
+
const edit = ["repo", "edit", slug, "--add-topic", topics.join(",")];
|
|
241
|
+
for (const args of [create, edit]) {
|
|
242
|
+
commands.push(render(args));
|
|
243
|
+
if (!dryRun)
|
|
244
|
+
gh(args, undefined, project.root);
|
|
245
|
+
}
|
|
246
|
+
return { repository: slug, visibility, topics, commands, dryRun };
|
|
247
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The I/O half of the OpenClaw surface: §8 C2, "mirror scaffolds by execution, not
|
|
3
|
+
* transcription". Runs the real `openclaw plugins init --type tool` into a temp directory and
|
|
4
|
+
* checks that everything it writes still agrees with what `plan()` emits, so an upstream default
|
|
5
|
+
* change is inherited on the next toolfactory release instead of silently rotting in a constant.
|
|
6
|
+
*
|
|
7
|
+
* Runnable as a script (`node dist/hosts/openclaw.js <root>`) because that is how the surface's
|
|
8
|
+
* `validate()` step reaches it: one Command like every other validator.
|
|
9
|
+
*/
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { argv, exit, stderr, stdout } from "node:process";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { loadProject } from "../project/load.js";
|
|
17
|
+
import { HOST_DIR, surface } from "../surfaces/openclaw-native.js";
|
|
18
|
+
export const DRIFT_ENTRY = fileURLToPath(import.meta.url);
|
|
19
|
+
/** Per-tool values: the scaffold writes its own, and the projection is expected to differ. */
|
|
20
|
+
const PER_TOOL_KEYS = new Set(["name", "version", "description"]);
|
|
21
|
+
/**
|
|
22
|
+
* Keys `tool.json` deliberately overrides (`openclaw.pluginApi` and the extra dependency maps):
|
|
23
|
+
* the scaffold's value is not law where the author has replaced it on purpose.
|
|
24
|
+
*/
|
|
25
|
+
function overridden(project) {
|
|
26
|
+
const openclaw = project.tool.openclaw;
|
|
27
|
+
const paths = new Set();
|
|
28
|
+
for (const section of ["dependencies", "peerDependencies", "devDependencies"]) {
|
|
29
|
+
for (const key of Object.keys(openclaw?.[section] ?? {}))
|
|
30
|
+
paths.add(`.${section}.${key}`);
|
|
31
|
+
}
|
|
32
|
+
if (openclaw?.pluginApi) {
|
|
33
|
+
paths.add(".peerDependencies.openclaw");
|
|
34
|
+
paths.add(".openclaw.compat.pluginApi");
|
|
35
|
+
}
|
|
36
|
+
return paths;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every scaffold key must survive into the projection with the same value; extra keys are the
|
|
40
|
+
* generator's own additions (the core dependency, the projected tools, the inspector fixture)
|
|
41
|
+
* and are allowed, as are the keys `tool.json` overrides on purpose.
|
|
42
|
+
*/
|
|
43
|
+
function missing(scaffold, ours, path, skip) {
|
|
44
|
+
if (skip.has(path))
|
|
45
|
+
return [];
|
|
46
|
+
if (scaffold === null || typeof scaffold !== "object" || Array.isArray(scaffold)) {
|
|
47
|
+
return JSON.stringify(scaffold) === JSON.stringify(ours)
|
|
48
|
+
? []
|
|
49
|
+
: [`${path}: upstream ${JSON.stringify(scaffold)}, generated ${JSON.stringify(ours)}`];
|
|
50
|
+
}
|
|
51
|
+
if (ours === null || typeof ours !== "object" || Array.isArray(ours)) {
|
|
52
|
+
return [`${path}: upstream object, generated ${JSON.stringify(ours)}`];
|
|
53
|
+
}
|
|
54
|
+
const right = ours;
|
|
55
|
+
return Object.entries(scaffold).flatMap(([key, value]) => path === "" && PER_TOOL_KEYS.has(key) ? [] : missing(value, right[key], `${path}.${key}`, skip));
|
|
56
|
+
}
|
|
57
|
+
function planned(files, path) {
|
|
58
|
+
const file = files.find((candidate) => candidate.path === `${HOST_DIR}/${path}`);
|
|
59
|
+
if (file?.kind !== "file")
|
|
60
|
+
throw new Error(`the openclaw surface no longer plans ${path}`);
|
|
61
|
+
return file.content;
|
|
62
|
+
}
|
|
63
|
+
/** Human-readable drift lines; empty means the generator still mirrors upstream. */
|
|
64
|
+
export function scaffoldDrift(project) {
|
|
65
|
+
const files = surface.plan(project);
|
|
66
|
+
const directory = mkdtempSync(join(tmpdir(), "toolfactory-openclaw-"));
|
|
67
|
+
// Prefer the openclaw the plugin pins as a devDependency over whatever is on PATH.
|
|
68
|
+
const pinned = join(project.root, HOST_DIR, "node_modules/.bin/openclaw");
|
|
69
|
+
try {
|
|
70
|
+
execFileSync(existsSync(pinned) ? pinned : "openclaw", ["plugins", "init", "tf-probe", "--type", "tool", "--directory", join(directory, "probe")], { stdio: "pipe" });
|
|
71
|
+
const scaffold = (path) => readFileSync(join(directory, "probe", path), "utf8");
|
|
72
|
+
const skip = overridden(project);
|
|
73
|
+
const drift = [
|
|
74
|
+
...["package.json", "tsconfig.json"].flatMap((path) => missing(JSON.parse(scaffold(path)), JSON.parse(planned(files, path)), "", skip).map((line) => `${path}${line}`)),
|
|
75
|
+
...(scaffold("vitest.config.ts") === planned(files, "vitest.config.ts")
|
|
76
|
+
? []
|
|
77
|
+
: ["vitest.config.ts: upstream template changed"]),
|
|
78
|
+
];
|
|
79
|
+
return drift;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
rmSync(directory, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (argv[1] === DRIFT_ENTRY) {
|
|
86
|
+
const drift = scaffoldDrift(loadProject(argv[2] ?? "."));
|
|
87
|
+
if (drift.length) {
|
|
88
|
+
stderr.write(`openclaw scaffold drifted upstream; update src/surfaces/openclaw-native.ts:\n${drift
|
|
89
|
+
.map((line) => ` ${line}`)
|
|
90
|
+
.join("\n")}\n`);
|
|
91
|
+
exit(1);
|
|
92
|
+
}
|
|
93
|
+
stdout.write("openclaw plugins init still matches src/surfaces/openclaw-native.ts\n");
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const DRIFT_ENTRY: string;
|
|
2
|
+
export declare const TEMPLATE_URL = "https://github.com/GoatInAHat/template";
|
|
3
|
+
/**
|
|
4
|
+
* The template files toolfactory projects into every generated repository. The two it does not
|
|
5
|
+
* vendor are the two it writes itself: `.agents/mcp/servers.json` (a merge file, so a teammate's
|
|
6
|
+
* absorbed server survives a rebuild) and `AGENTS.md` (a region file with the tool's own prose).
|
|
7
|
+
*/
|
|
8
|
+
export declare const VENDORED_PATHS: string[];
|
|
9
|
+
/** Human-readable drift lines; empty means the vendored copy is still the template's own bytes. */
|
|
10
|
+
export declare function scaffoldDrift(source?: string): string[];
|
|
11
|
+
/** Re-vendor: read every projected file out of a checkout and rewrite the constants module. */
|
|
12
|
+
export declare function vendor(source?: string): {
|
|
13
|
+
commit: string;
|
|
14
|
+
path: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The I/O half of the `agents` surface: §8 C2, "mirror scaffolds by execution, not
|
|
3
|
+
* transcription", applied to a repository instead of a generator. The agent-config machinery
|
|
4
|
+
* every generated project carries — `.agents/sync.py`, the hook carriers, the agent-config
|
|
5
|
+
* workflow — is one shared contract owned by github.com/GoatInAHat/template, and a copy of a
|
|
6
|
+
* moving contract rots silently. So the bytes are vendored into `src/surfaces/agents.template.ts`
|
|
7
|
+
* by this module's `--write` mode, and the copy is proved by cloning the real repository and
|
|
8
|
+
* diffing it file by file.
|
|
9
|
+
*
|
|
10
|
+
* Runnable as a script (`node dist/hosts/template.js [<url or checkout>] [--write]`) because that
|
|
11
|
+
* is how the surface's `validate()` step reaches it: one Command like every other validator.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { argv, exit, stderr, stdout } from "node:process";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { TEMPLATE_COMMIT, TEMPLATE_FILES } from "../surfaces/agents.template.js";
|
|
20
|
+
export const DRIFT_ENTRY = fileURLToPath(import.meta.url);
|
|
21
|
+
export const TEMPLATE_URL = "https://github.com/GoatInAHat/template";
|
|
22
|
+
/**
|
|
23
|
+
* The template files toolfactory projects into every generated repository. The two it does not
|
|
24
|
+
* vendor are the two it writes itself: `.agents/mcp/servers.json` (a merge file, so a teammate's
|
|
25
|
+
* absorbed server survives a rebuild) and `AGENTS.md` (a region file with the tool's own prose).
|
|
26
|
+
*/
|
|
27
|
+
export const VENDORED_PATHS = [
|
|
28
|
+
".agents/README.md",
|
|
29
|
+
".agents/setup",
|
|
30
|
+
".agents/skills/.gitkeep",
|
|
31
|
+
".agents/sync.py",
|
|
32
|
+
".claude/settings.json",
|
|
33
|
+
".cursor/environment.json",
|
|
34
|
+
".devcontainer/Dockerfile",
|
|
35
|
+
".devcontainer/devcontainer.json",
|
|
36
|
+
".gitattributes",
|
|
37
|
+
".github/workflows/agent-config.yml",
|
|
38
|
+
".github/workflows/copilot-setup-steps.yml",
|
|
39
|
+
".gitignore",
|
|
40
|
+
];
|
|
41
|
+
/** Where `--write` puts the vendored bytes; a source-tree path, since re-vendoring is a dev step. */
|
|
42
|
+
const VENDORED_MODULE = join(dirname(DRIFT_ENTRY), "..", "surfaces", "agents.template.ts");
|
|
43
|
+
function head(root) {
|
|
44
|
+
return execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
45
|
+
}
|
|
46
|
+
/** A local checkout is used where it is; anything else is cloned shallow into a temp directory. */
|
|
47
|
+
function checkout(source) {
|
|
48
|
+
const local = (() => {
|
|
49
|
+
try {
|
|
50
|
+
return statSync(source).isDirectory();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
if (local)
|
|
57
|
+
return { root: source, commit: head(source), dispose: () => { } };
|
|
58
|
+
const directory = mkdtempSync(join(tmpdir(), "toolfactory-template-"));
|
|
59
|
+
execFileSync("git", ["clone", "--depth", "1", source, directory], { stdio: "pipe" });
|
|
60
|
+
return {
|
|
61
|
+
root: directory,
|
|
62
|
+
commit: head(directory),
|
|
63
|
+
dispose: () => rmSync(directory, { recursive: true, force: true }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function read(root, path) {
|
|
67
|
+
try {
|
|
68
|
+
return readFileSync(join(root, path), "utf8");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Human-readable drift lines; empty means the vendored copy is still the template's own bytes. */
|
|
75
|
+
export function scaffoldDrift(source = TEMPLATE_URL) {
|
|
76
|
+
const at = checkout(source);
|
|
77
|
+
try {
|
|
78
|
+
return VENDORED_PATHS.flatMap((path) => {
|
|
79
|
+
const upstream = read(at.root, path);
|
|
80
|
+
if (upstream === undefined)
|
|
81
|
+
return [`${path}: no longer in the template`];
|
|
82
|
+
if (TEMPLATE_FILES[path] === undefined)
|
|
83
|
+
return [`${path}: not vendored`];
|
|
84
|
+
return upstream === TEMPLATE_FILES[path] ? [] : [`${path}: the template moved`];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
at.dispose();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function moduleSource(files, commit) {
|
|
92
|
+
return [
|
|
93
|
+
"/**",
|
|
94
|
+
` * Vendored from ${TEMPLATE_URL} at ${commit}.`,
|
|
95
|
+
" *",
|
|
96
|
+
" * Do not edit: written by `pnpm vendor:template`, proved by `src/hosts/template.ts`'s",
|
|
97
|
+
" * scaffoldDrift, which the `agents` surface's validate() runs against the real repository.",
|
|
98
|
+
" */",
|
|
99
|
+
`export const TEMPLATE_COMMIT = ${JSON.stringify(commit)};`,
|
|
100
|
+
"",
|
|
101
|
+
"export const TEMPLATE_FILES: Record<string, string> = {",
|
|
102
|
+
...VENDORED_PATHS.map((path) => ` ${JSON.stringify(path)}: ${JSON.stringify(files[path])},`),
|
|
103
|
+
"};",
|
|
104
|
+
"",
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
/** Re-vendor: read every projected file out of a checkout and rewrite the constants module. */
|
|
108
|
+
export function vendor(source = TEMPLATE_URL) {
|
|
109
|
+
const at = checkout(source);
|
|
110
|
+
try {
|
|
111
|
+
const files = Object.fromEntries(VENDORED_PATHS.map((path) => {
|
|
112
|
+
const content = read(at.root, path);
|
|
113
|
+
if (content === undefined)
|
|
114
|
+
throw new Error(`${path} is not in ${source}`);
|
|
115
|
+
return [path, content];
|
|
116
|
+
}));
|
|
117
|
+
writeFileSync(VENDORED_MODULE, moduleSource(files, at.commit));
|
|
118
|
+
return { commit: at.commit, path: VENDORED_MODULE };
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
at.dispose();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (argv[1] === DRIFT_ENTRY) {
|
|
125
|
+
const args = argv.slice(2);
|
|
126
|
+
const write = args.includes("--write");
|
|
127
|
+
const source = args.find((argument) => !argument.startsWith("--")) ?? TEMPLATE_URL;
|
|
128
|
+
if (write) {
|
|
129
|
+
const { commit, path } = vendor(source);
|
|
130
|
+
stdout.write(`vendored ${VENDORED_PATHS.length} template files at ${commit} into ${path}\n`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const drift = scaffoldDrift(source);
|
|
134
|
+
if (drift.length) {
|
|
135
|
+
stderr.write(`the agent-config template drifted; re-run \`pnpm vendor:template\`:\n${drift
|
|
136
|
+
.map((line) => ` ${line}`)
|
|
137
|
+
.join("\n")}\n`);
|
|
138
|
+
exit(1);
|
|
139
|
+
}
|
|
140
|
+
stdout.write(`the template still matches src/surfaces/agents.template.ts (vendored at ${TEMPLATE_COMMIT})\n`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The I/O half of the web surface: §8 C2, "mirror scaffolds by execution, not transcription".
|
|
3
|
+
* Runs the real generators — `npm create vite`, the Tailwind and alias steps from
|
|
4
|
+
* ui.shadcn.com/docs/installation/vite, `shadcn init` and `shadcn add` — into a temp directory,
|
|
5
|
+
* seeding it with the very files `plan()` emits, then checks that everything they write still
|
|
6
|
+
* agrees with WEB_SCAFFOLD, so an upstream default change is inherited on the next toolfactory
|
|
7
|
+
* release instead of silently rotting in a constant.
|
|
8
|
+
*
|
|
9
|
+
* Runnable as a script (`node dist/hosts/web.js <root>`) because that is how the surface's
|
|
10
|
+
* `validate()` step reaches it: one Command like every other validator.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { argv, exit, stderr, stdout } from "node:process";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { loadProject } from "../project/load.js";
|
|
19
|
+
import { surface, TAILWIND_CSS, WEB_DIR, WEB_SCAFFOLD } from "../surfaces/web.js";
|
|
20
|
+
export const DRIFT_ENTRY = fileURLToPath(import.meta.url);
|
|
21
|
+
/** The scaffold's own config, which the generators read before writing anything else. */
|
|
22
|
+
const SEEDED = ["vite.config.ts", "tsconfig.json", "tsconfig.app.json"];
|
|
23
|
+
function planned(files, path) {
|
|
24
|
+
const file = files.find((candidate) => candidate.path === `${WEB_DIR}/${path}`);
|
|
25
|
+
if (file?.kind !== "file")
|
|
26
|
+
throw new Error(`the web surface no longer plans ${path}`);
|
|
27
|
+
return file.content;
|
|
28
|
+
}
|
|
29
|
+
/** JSON with the comments a tsconfig is allowed to carry. */
|
|
30
|
+
function parse(text) {
|
|
31
|
+
return JSON.parse(text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""));
|
|
32
|
+
}
|
|
33
|
+
function differs(label, upstream, pinned) {
|
|
34
|
+
return JSON.stringify(upstream) === JSON.stringify(pinned)
|
|
35
|
+
? []
|
|
36
|
+
: [`${label}: upstream and WEB_SCAFFOLD no longer agree`];
|
|
37
|
+
}
|
|
38
|
+
/** Human-readable drift lines; empty means the generators still write what WEB_SCAFFOLD says. */
|
|
39
|
+
export function scaffoldDrift(project) {
|
|
40
|
+
const files = surface.plan(project);
|
|
41
|
+
const directory = mkdtempSync(join(tmpdir(), "toolfactory-web-"));
|
|
42
|
+
const probe = join(directory, WEB_DIR);
|
|
43
|
+
// Prefer the shadcn the page pins as a dependency over whatever is on PATH.
|
|
44
|
+
const pinned = join(project.root, WEB_DIR, "node_modules/.bin/shadcn");
|
|
45
|
+
const shadcn = (args) => existsSync(pinned)
|
|
46
|
+
? execFileSync(pinned, [...args, "--cwd", probe], { stdio: "pipe" })
|
|
47
|
+
: execFileSync("npx", ["--yes", "shadcn@latest", ...args, "--cwd", probe], { stdio: "pipe" });
|
|
48
|
+
try {
|
|
49
|
+
execFileSync("npm", ["create", "vite@latest", WEB_DIR, "--", "--template", WEB_SCAFFOLD.viteTemplate], { cwd: directory, stdio: "pipe" });
|
|
50
|
+
const scaffold = (path) => readFileSync(join(probe, path), "utf8");
|
|
51
|
+
const drift = Object.keys(WEB_SCAFFOLD.vite).flatMap((path) => scaffold(path) === WEB_SCAFFOLD.vite[path]
|
|
52
|
+
? []
|
|
53
|
+
: [`${path}: \`npm create vite\` no longer writes what WEB_SCAFFOLD.vite pins`]);
|
|
54
|
+
// The documentation's Tailwind and alias steps, as the generated project carries them.
|
|
55
|
+
writeFileSync(join(probe, "src/index.css"), TAILWIND_CSS);
|
|
56
|
+
for (const path of SEEDED)
|
|
57
|
+
writeFileSync(join(probe, path), planned(files, path));
|
|
58
|
+
execFileSync("npm", ["--prefix", probe, "install", "tailwindcss", "@tailwindcss/vite"], {
|
|
59
|
+
stdio: "pipe",
|
|
60
|
+
});
|
|
61
|
+
shadcn(["init", "--yes", "--base", WEB_SCAFFOLD.base, "--preset", WEB_SCAFFOLD.preset]);
|
|
62
|
+
shadcn(["add", "--yes", ...WEB_SCAFFOLD.components]);
|
|
63
|
+
const packageJson = JSON.parse(scaffold("package.json"));
|
|
64
|
+
return [
|
|
65
|
+
...drift,
|
|
66
|
+
...["scripts", "dependencies", "devDependencies"].flatMap((key) => differs(`package.json ${key}`, packageJson[key], WEB_SCAFFOLD.packageJson[key])),
|
|
67
|
+
...differs("components.json", parse(scaffold("components.json")), WEB_SCAFFOLD.componentsJson),
|
|
68
|
+
...(scaffold("src/index.css") === WEB_SCAFFOLD.indexCss
|
|
69
|
+
? []
|
|
70
|
+
: ["src/index.css: `shadcn init` no longer writes what WEB_SCAFFOLD.indexCss pins"]),
|
|
71
|
+
...(scaffold("src/lib/utils.ts") === WEB_SCAFFOLD.utils
|
|
72
|
+
? []
|
|
73
|
+
: ["src/lib/utils.ts: `shadcn init` no longer writes what WEB_SCAFFOLD.utils pins"]),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(directory, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (argv[1] === DRIFT_ENTRY) {
|
|
81
|
+
const drift = scaffoldDrift(loadProject(argv[2] ?? "."));
|
|
82
|
+
if (drift.length) {
|
|
83
|
+
stderr.write(`web scaffold drifted upstream; update WEB_SCAFFOLD in src/surfaces/web.ts:\n${drift
|
|
84
|
+
.map((line) => ` ${line}`)
|
|
85
|
+
.join("\n")}\n`);
|
|
86
|
+
exit(1);
|
|
87
|
+
}
|
|
88
|
+
stdout.write("the vite + shadcn scaffold still matches src/surfaces/web.ts\n");
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function assertValidName(name: string): void;
|
|
2
|
+
/** The total projection of the canonical name N onto every wire-visible identifier. */
|
|
3
|
+
export declare const projectName: {
|
|
4
|
+
npm: (name: string, scope?: string) => string;
|
|
5
|
+
pypi: (name: string) => string;
|
|
6
|
+
pythonPackage: (name: string) => string;
|
|
7
|
+
/** Human-facing name for manifests: words from the hyphens and dots, capitalised. */
|
|
8
|
+
display: (name: string) => string;
|
|
9
|
+
cursor: (name: string) => string;
|
|
10
|
+
/** Gemini CLI extension name: the directory name rule, the same shape as Cursor's. */
|
|
11
|
+
gemini: (name: string) => string;
|
|
12
|
+
openclawPackage: (name: string) => string;
|
|
13
|
+
mcpRegistry: (name: string, owner: string) => string;
|
|
14
|
+
hermesToolset: (name: string) => string;
|
|
15
|
+
dshPackage: (name: string) => string;
|
|
16
|
+
/** DSH `serverName`: `[A-Za-z0-9_-]{1,32}`, validated at boot, and wire-visible as `mcp__<it>__<tool>`. */
|
|
17
|
+
dshServer: (name: string) => string;
|
|
18
|
+
};
|
|
19
|
+
/** `owner` from a GitHub repository URL, for the MCP Registry namespace. */
|
|
20
|
+
export declare function githubOwner(repository: string | undefined): string | undefined;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NAME_PATTERN } from "../model.js";
|
|
2
|
+
export function assertValidName(name) {
|
|
3
|
+
if (!NAME_PATTERN.test(name) || name.length > 64) {
|
|
4
|
+
throw new Error(`Invalid tool name ${JSON.stringify(name)}: must match Agent Plugins ${NAME_PATTERN} and be at most 64 characters.`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/** The total projection of the canonical name N onto every wire-visible identifier. */
|
|
8
|
+
export const projectName = {
|
|
9
|
+
npm: (name, scope) => scope ? `@${scope.replace(/^@/, "")}/${name}` : name,
|
|
10
|
+
pypi: (name) => name.replace(/\./g, "-"),
|
|
11
|
+
pythonPackage: (name) => name.replace(/[.-]/g, "_").replace(/^(?=\d)/, "_"),
|
|
12
|
+
/** Human-facing name for manifests: words from the hyphens and dots, capitalised. */
|
|
13
|
+
display: (name) => name
|
|
14
|
+
.split(/[-.]/)
|
|
15
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
16
|
+
.join(" "),
|
|
17
|
+
cursor: (name) => name.replace(/\./g, "-"),
|
|
18
|
+
/** Gemini CLI extension name: the directory name rule, the same shape as Cursor's. */
|
|
19
|
+
gemini: (name) => name.replace(/\./g, "-"),
|
|
20
|
+
openclawPackage: (name) => `openclaw-plugin-${name}`,
|
|
21
|
+
mcpRegistry: (name, owner) => `io.github.${owner}/${name}`,
|
|
22
|
+
hermesToolset: (name) => name.replace(/[.-]/g, "_"),
|
|
23
|
+
dshPackage: (name) => `${name}-dsh`,
|
|
24
|
+
/** DSH `serverName`: `[A-Za-z0-9_-]{1,32}`, validated at boot, and wire-visible as `mcp__<it>__<tool>`. */
|
|
25
|
+
dshServer: (name) => name.replace(/\./g, "-").slice(0, 32),
|
|
26
|
+
};
|
|
27
|
+
/** `owner` from a GitHub repository URL, for the MCP Registry namespace. */
|
|
28
|
+
export function githubOwner(repository) {
|
|
29
|
+
if (!repository)
|
|
30
|
+
return undefined;
|
|
31
|
+
const match = repository.match(/github\.com[/:]([^/]+)\/[^/#?]+/);
|
|
32
|
+
return match?.[1];
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|