@workbench-ai/workbench 0.0.73 → 0.0.75

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.
@@ -0,0 +1,13 @@
1
+ export interface FanOutResult {
2
+ status: "completed" | "skipped" | "failed";
3
+ command: string;
4
+ linkedAgents: string[];
5
+ additionalAgents?: number;
6
+ reason?: string;
7
+ exitCode?: number | null;
8
+ }
9
+ export declare function fanOutSkill(name: string, options: {
10
+ skillDir: string;
11
+ }): Promise<FanOutResult>;
12
+ export declare function manualFanOutCommand(skillDir: string, name: string): string;
13
+ //# sourceMappingURL=fanout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fanout.d.ts","sourceRoot":"","sources":["../src/fanout.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAoEpG;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAO1E"}
package/dist/fanout.js ADDED
@@ -0,0 +1,223 @@
1
+ import { spawn } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ const require = createRequire(import.meta.url);
7
+ const FANOUT_TIMEOUT_MS = 30_000;
8
+ export async function fanOutSkill(name, options) {
9
+ const resolved = await resolveSkillsBin().catch((error) => ({
10
+ binPath: null,
11
+ reason: error instanceof Error ? error.message : "Unable to resolve skills package.",
12
+ }));
13
+ const command = manualFanOutCommand(options.skillDir, name);
14
+ if (!resolved.binPath) {
15
+ return {
16
+ status: "skipped",
17
+ command,
18
+ linkedAgents: [],
19
+ reason: resolved.reason,
20
+ };
21
+ }
22
+ const stagingRoot = await fs.mkdtemp(path.join(os.tmpdir(), "workbench-skills-fanout-"));
23
+ const stagedSkillDir = path.join(stagingRoot, name);
24
+ try {
25
+ await fs.cp(options.skillDir, stagedSkillDir, {
26
+ recursive: true,
27
+ force: true,
28
+ dereference: true,
29
+ });
30
+ const args = [
31
+ resolved.binPath,
32
+ "add",
33
+ stagedSkillDir,
34
+ "--global",
35
+ "--yes",
36
+ ];
37
+ const child = await runNode(args, minimalChildEnv(process.env), FANOUT_TIMEOUT_MS);
38
+ const { linkedAgents, additionalAgents } = parseFanOutAgents(`${child.stdout}\n${child.stderr}`);
39
+ if (child.timedOut) {
40
+ return {
41
+ status: "failed",
42
+ command,
43
+ linkedAgents,
44
+ ...(additionalAgents > 0 ? { additionalAgents } : {}),
45
+ reason: `skills fan-out timed out after ${FANOUT_TIMEOUT_MS}ms.`,
46
+ exitCode: child.exitCode,
47
+ };
48
+ }
49
+ if (child.exitCode !== 0) {
50
+ return {
51
+ status: "failed",
52
+ command,
53
+ linkedAgents,
54
+ ...(additionalAgents > 0 ? { additionalAgents } : {}),
55
+ reason: conciseFailureReason(`${child.stdout}\n${child.stderr}`) ?? `skills fan-out exited ${child.exitCode}.`,
56
+ exitCode: child.exitCode,
57
+ };
58
+ }
59
+ return {
60
+ status: "completed",
61
+ command,
62
+ linkedAgents,
63
+ ...(additionalAgents > 0 ? { additionalAgents } : {}),
64
+ exitCode: child.exitCode,
65
+ };
66
+ }
67
+ catch (error) {
68
+ return {
69
+ status: "failed",
70
+ command,
71
+ linkedAgents: [],
72
+ reason: error instanceof Error ? error.message : "skills fan-out failed.",
73
+ };
74
+ }
75
+ finally {
76
+ await fs.rm(stagingRoot, { recursive: true, force: true });
77
+ }
78
+ }
79
+ export function manualFanOutCommand(skillDir, name) {
80
+ const quotedName = quoteShellArg(name);
81
+ return [
82
+ "tmpdir=$(mktemp -d)",
83
+ `cp -R ${quoteShellArg(skillDir)} "$tmpdir"/${quotedName}`,
84
+ `skills add "$tmpdir"/${quotedName} --global --yes`,
85
+ ].join(" && ");
86
+ }
87
+ function quoteShellArg(value) {
88
+ return /^[A-Za-z0-9_./:=@+-]+$/u.test(value)
89
+ ? value
90
+ : `'${value.replace(/'/gu, "'\\''")}'`;
91
+ }
92
+ async function resolveSkillsBin() {
93
+ const packageJsonPath = require.resolve("skills/package.json");
94
+ const packageRoot = path.dirname(packageJsonPath);
95
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
96
+ const bin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.skills;
97
+ if (!bin) {
98
+ throw new Error("The skills package does not declare a skills bin.");
99
+ }
100
+ return {
101
+ binPath: path.resolve(packageRoot, bin),
102
+ };
103
+ }
104
+ function runNode(args, env, timeoutMs) {
105
+ return new Promise((resolve) => {
106
+ const child = spawn(process.execPath, args, {
107
+ env,
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ windowsHide: true,
110
+ });
111
+ let stdout = "";
112
+ let stderr = "";
113
+ let timedOut = false;
114
+ const timer = setTimeout(() => {
115
+ timedOut = true;
116
+ child.kill("SIGTERM");
117
+ }, timeoutMs);
118
+ child.stdout?.setEncoding("utf8");
119
+ child.stderr?.setEncoding("utf8");
120
+ child.stdout?.on("data", (chunk) => {
121
+ stdout += chunk;
122
+ });
123
+ child.stderr?.on("data", (chunk) => {
124
+ stderr += chunk;
125
+ });
126
+ child.on("error", (error) => {
127
+ clearTimeout(timer);
128
+ resolve({
129
+ exitCode: 1,
130
+ stdout,
131
+ stderr: stderr || error.message,
132
+ timedOut,
133
+ });
134
+ });
135
+ child.on("close", (exitCode) => {
136
+ clearTimeout(timer);
137
+ resolve({
138
+ exitCode,
139
+ stdout,
140
+ stderr,
141
+ timedOut,
142
+ });
143
+ });
144
+ });
145
+ }
146
+ // The skills CLI changes behavior when it detects it is running inside an
147
+ // agent (via @vercel/detect-agent reading agent-specific environment
148
+ // variables). Fan-out must always run in machine mode, so the child gets a
149
+ // minimal allowlisted environment instead of a hand-maintained blacklist of
150
+ // every agent's marker variables.
151
+ function minimalChildEnv(env) {
152
+ const allowed = [
153
+ "PATH",
154
+ "HOME",
155
+ "USER",
156
+ "SHELL",
157
+ "TMPDIR",
158
+ "TMP",
159
+ "TEMP",
160
+ "LANG",
161
+ "NODE_EXTRA_CA_CERTS",
162
+ "HTTP_PROXY",
163
+ "HTTPS_PROXY",
164
+ "NO_PROXY",
165
+ "http_proxy",
166
+ "https_proxy",
167
+ "no_proxy",
168
+ "SYSTEMROOT",
169
+ "SystemRoot",
170
+ "COMSPEC",
171
+ "APPDATA",
172
+ "LOCALAPPDATA",
173
+ "USERPROFILE",
174
+ ];
175
+ const next = {};
176
+ for (const key of Object.keys(env)) {
177
+ if (allowed.includes(key) || key.startsWith("XDG_") || key.startsWith("LC_")) {
178
+ next[key] = env[key];
179
+ }
180
+ }
181
+ return next;
182
+ }
183
+ function parseFanOutAgents(output) {
184
+ const cleaned = stripAnsi(output);
185
+ const linked = new Set();
186
+ let additionalAgents = 0;
187
+ for (const line of cleaned.split(/\r?\n/u)) {
188
+ const normalized = line.replace(/[\u2502\u25c7\u25cf\u25a0\u2713\u2717]/gu, " ").trim();
189
+ const match = /^(?:universal|symlinked|copied|copy\s*\u2192|symlink\s*\u2192):\s*(.+)$/u.exec(normalized);
190
+ if (!match) {
191
+ continue;
192
+ }
193
+ for (const rawAgent of match[1].split(",").map((entry) => entry.trim())) {
194
+ const moreMatch = /\+(\d+) more$/u.exec(rawAgent);
195
+ if (moreMatch) {
196
+ additionalAgents += Number(moreMatch[1]);
197
+ }
198
+ const agent = rawAgent.replace(/\s*\+\d+ more$/u, "").trim();
199
+ if (agent) {
200
+ linked.add(agent);
201
+ }
202
+ }
203
+ }
204
+ return {
205
+ linkedAgents: [...linked].sort((left, right) => left.localeCompare(right)),
206
+ additionalAgents,
207
+ };
208
+ }
209
+ function conciseFailureReason(output) {
210
+ const lines = stripAnsi(output)
211
+ .split(/\r?\n/u)
212
+ .map((line) => line.replace(/[\u2500-\u257f\u25a0\u25cf\u25c7\u2713\u2717]/gu, " ").trim())
213
+ .filter((line) => line.length > 0 &&
214
+ !/^skills\s*$/iu.test(line) &&
215
+ !/^Tip:/u.test(line) &&
216
+ !/^(Source|Local path|Found \d+ skill|Available skills|Installed \d+ skill|Done!|Review skills)/u.test(line) &&
217
+ !/^- /u.test(line));
218
+ const reason = lines.at(-1);
219
+ return reason ? (reason.length > 200 ? `${reason.slice(0, 197)}...` : reason) : null;
220
+ }
221
+ function stripAnsi(value) {
222
+ return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/gu, "");
223
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2DA,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B;AAsUD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,GAAE,KAGzD,GAAG,OAAO,CAAC,MAAM,CAAC,CAiMlB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgEA,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;CAC/B;AAuTD,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,GAAE,KAGzD,GAAG,OAAO,CAAC,MAAM,CAAC,CAoMlB"}