@webgrow/skillhub 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/bridge/.env.example +14 -0
- package/bridge/README.md +74 -0
- package/bridge/adapters.mjs +209 -0
- package/bridge/convex-functions.mjs +13 -0
- package/bridge/doctor.mjs +448 -0
- package/bridge/harness.mjs +217 -0
- package/convex/_generated/ai/ai-files.state.json +6 -0
- package/convex/_generated/ai/guidelines.md +368 -0
- package/convex/_generated/api.d.ts +59 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/bridge.ts +709 -0
- package/convex/convex.config.ts +8 -0
- package/convex/http.ts +37 -0
- package/convex/loops.ts +546 -0
- package/convex/runs.ts +183 -0
- package/convex/schema.ts +220 -0
- package/convex/skills.ts +413 -0
- package/convex/tsconfig.json +25 -0
- package/dashboard/.env.example +1 -0
- package/dashboard/index.html +12 -0
- package/dashboard/src/App.jsx +1743 -0
- package/dashboard/src/convexRefs.js +32 -0
- package/dashboard/src/main.jsx +46 -0
- package/dashboard/src/styles.css +982 -0
- package/dashboard/tsconfig.json +17 -0
- package/dashboard/vite.config.mjs +23 -0
- package/package.json +48 -0
- package/src/bridge-command.mjs +101 -0
- package/src/cli.mjs +246 -0
- package/src/cloud-catalog.mjs +588 -0
- package/src/config.mjs +150 -0
- package/src/connect.mjs +410 -0
package/src/config.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const defaultCodexArgs = [
|
|
7
|
+
"--ask-for-approval",
|
|
8
|
+
"never",
|
|
9
|
+
"exec",
|
|
10
|
+
"--sandbox",
|
|
11
|
+
"workspace-write",
|
|
12
|
+
"--json",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const configFileName = "config.json";
|
|
16
|
+
|
|
17
|
+
export function getConfigDir(env = process.env) {
|
|
18
|
+
return env.SKILLHUB_CONFIG_DIR || path.join(os.homedir(), ".skillhub");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConfigPath(env = process.env) {
|
|
22
|
+
return path.join(getConfigDir(env), configFileName);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function defaultConfig() {
|
|
26
|
+
return {
|
|
27
|
+
workspaceUrl: "",
|
|
28
|
+
convexUrl: "",
|
|
29
|
+
codexCommand: "codex",
|
|
30
|
+
codexArgs: defaultCodexArgs,
|
|
31
|
+
codexWorkdir: process.cwd(),
|
|
32
|
+
bridgeMode: "codex-cli",
|
|
33
|
+
bridge: {
|
|
34
|
+
pid: null,
|
|
35
|
+
logPath: path.join(getConfigDir(), "bridge.log"),
|
|
36
|
+
startedAt: null,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readUserConfig(env = process.env) {
|
|
42
|
+
const configPath = getConfigPath(env);
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(configPath, "utf8");
|
|
45
|
+
return normalizeConfig(JSON.parse(raw));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error?.code === "ENOENT") return defaultConfig();
|
|
48
|
+
throw new Error(`Could not read SkillHub config at ${configPath}: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readUserConfigSync(env = process.env) {
|
|
53
|
+
const configPath = getConfigPath(env);
|
|
54
|
+
if (!existsSync(configPath)) return defaultConfig();
|
|
55
|
+
try {
|
|
56
|
+
return normalizeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Could not read SkillHub config at ${configPath}: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function writeUserConfig(config, env = process.env) {
|
|
63
|
+
const normalized = normalizeConfig(config);
|
|
64
|
+
const configDir = getConfigDir(env);
|
|
65
|
+
await mkdir(configDir, { recursive: true });
|
|
66
|
+
await writeFile(
|
|
67
|
+
path.join(configDir, configFileName),
|
|
68
|
+
`${JSON.stringify(normalized, null, 2)}\n`,
|
|
69
|
+
"utf8",
|
|
70
|
+
);
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getRuntimeConfig(env = process.env) {
|
|
75
|
+
const config = readUserConfigSync(env);
|
|
76
|
+
return {
|
|
77
|
+
...config,
|
|
78
|
+
convexUrl: config.convexUrl || env.CONVEX_URL || env.VITE_CONVEX_URL || "",
|
|
79
|
+
workspaceUrl: config.workspaceUrl || env.SKILLHUB_WORKSPACE_URL || "",
|
|
80
|
+
codexCommand: config.codexCommand || env.CODEX_COMMAND || "codex",
|
|
81
|
+
codexArgs: arrayOrDefault(
|
|
82
|
+
config.codexArgs,
|
|
83
|
+
parseJsonArray(env.CODEX_ARGS_JSON) || defaultCodexArgs,
|
|
84
|
+
),
|
|
85
|
+
codexWorkdir: config.codexWorkdir || env.CODEX_WORKDIR || process.cwd(),
|
|
86
|
+
bridgeMode:
|
|
87
|
+
config.bridgeMode || env.SKILLHUB_HARNESS_MODE || env.SKILLHUB_BRIDGE_MODE || "codex-cli",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createBridgeEnv(config = getRuntimeConfig(), env = process.env) {
|
|
92
|
+
return {
|
|
93
|
+
...env,
|
|
94
|
+
CONVEX_URL: config.convexUrl || env.CONVEX_URL || "",
|
|
95
|
+
SKILLHUB_HARNESS_MODE: config.bridgeMode || "codex-cli",
|
|
96
|
+
CODEX_COMMAND: config.codexCommand || "codex",
|
|
97
|
+
CODEX_ARGS_JSON: JSON.stringify(arrayOrDefault(config.codexArgs, defaultCodexArgs)),
|
|
98
|
+
CODEX_WORKDIR: config.codexWorkdir || process.cwd(),
|
|
99
|
+
SKILLHUB_CAPABILITIES:
|
|
100
|
+
env.SKILLHUB_CAPABILITIES ||
|
|
101
|
+
"codex.cli,codex.create_goal,codex.create_heartbeat,codex.create_thread,codex.notify_user",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeConfig(config) {
|
|
106
|
+
const base = defaultConfig();
|
|
107
|
+
const bridge = {
|
|
108
|
+
...base.bridge,
|
|
109
|
+
...(isObject(config?.bridge) ? config.bridge : {}),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
workspaceUrl: stringOrEmpty(config?.workspaceUrl),
|
|
114
|
+
convexUrl: stringOrEmpty(config?.convexUrl),
|
|
115
|
+
codexCommand: stringOrDefault(config?.codexCommand, base.codexCommand),
|
|
116
|
+
codexArgs: arrayOrDefault(config?.codexArgs, base.codexArgs),
|
|
117
|
+
codexWorkdir: stringOrDefault(config?.codexWorkdir, base.codexWorkdir),
|
|
118
|
+
bridgeMode: stringOrDefault(config?.bridgeMode, base.bridgeMode),
|
|
119
|
+
bridge,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseJsonArray(value) {
|
|
124
|
+
if (!value) return null;
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(value);
|
|
127
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function arrayOrDefault(value, fallback) {
|
|
134
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
135
|
+
? value
|
|
136
|
+
: fallback;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stringOrEmpty(value) {
|
|
140
|
+
return typeof value === "string" ? value.trim() : "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function stringOrDefault(value, fallback) {
|
|
144
|
+
const cleaned = stringOrEmpty(value);
|
|
145
|
+
return cleaned || fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isObject(value) {
|
|
149
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
150
|
+
}
|
package/src/connect.mjs
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, open, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
createBridgeEnv,
|
|
11
|
+
defaultCodexArgs,
|
|
12
|
+
getConfigDir,
|
|
13
|
+
getConfigPath,
|
|
14
|
+
readUserConfig,
|
|
15
|
+
writeUserConfig,
|
|
16
|
+
} from "./config.mjs";
|
|
17
|
+
import { getCloudCatalogItems } from "./cloud-catalog.mjs";
|
|
18
|
+
|
|
19
|
+
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const packageRoot = path.dirname(srcDir);
|
|
21
|
+
const cliPath = path.join(srcDir, "cli.mjs");
|
|
22
|
+
const bridgePath = path.join(packageRoot, "bridge", "harness.mjs");
|
|
23
|
+
const packageName = "@webgrow/skillhub";
|
|
24
|
+
|
|
25
|
+
export async function runConnectCommand(argv = [], io = process) {
|
|
26
|
+
const flags = parseFlags(argv);
|
|
27
|
+
const config = await readUserConfig();
|
|
28
|
+
const rl = io.stdin?.isTTY
|
|
29
|
+
? createInterface({ input: io.stdin, output: io.stdout })
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
console.log("Checking Codex...");
|
|
34
|
+
let codexCommand = flags.codexCommand || config.codexCommand || "codex";
|
|
35
|
+
let codex = await checkCommand(codexCommand);
|
|
36
|
+
if (!codex.ok && !flags.codexCommand) {
|
|
37
|
+
const configuredCodex = await findConfiguredCodexCommand();
|
|
38
|
+
if (configuredCodex?.ok) {
|
|
39
|
+
codexCommand = configuredCodex.command;
|
|
40
|
+
codex = configuredCodex;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!codex.ok) {
|
|
44
|
+
if (codex.missing) {
|
|
45
|
+
await maybeInstallCodex({ rl, yes: flags.yes });
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Codex was found but could not start: ${codex.detail}`);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
console.log("Codex is installed.");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("Checking login...");
|
|
54
|
+
if (await isCodexSignedIn()) {
|
|
55
|
+
console.log("Codex is signed in.");
|
|
56
|
+
} else {
|
|
57
|
+
await runCodexLogin({ command: codexCommand, rl, yes: flags.yes });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log("Connecting SkillHub...");
|
|
61
|
+
const workspaceUrl =
|
|
62
|
+
flags.workspaceUrl ||
|
|
63
|
+
config.workspaceUrl ||
|
|
64
|
+
process.env.SKILLHUB_WORKSPACE_URL ||
|
|
65
|
+
(await askOptional(rl, "SkillHub dashboard URL (optional): "));
|
|
66
|
+
const convexUrl =
|
|
67
|
+
flags.convexUrl ||
|
|
68
|
+
config.convexUrl ||
|
|
69
|
+
process.env.CONVEX_URL ||
|
|
70
|
+
process.env.VITE_CONVEX_URL ||
|
|
71
|
+
(await discoverConnectionUrl(workspaceUrl)) ||
|
|
72
|
+
(await askRequired(rl, "SkillHub connection URL: "));
|
|
73
|
+
|
|
74
|
+
const nextConfig = await writeUserConfig({
|
|
75
|
+
...config,
|
|
76
|
+
workspaceUrl,
|
|
77
|
+
convexUrl,
|
|
78
|
+
codexCommand,
|
|
79
|
+
codexArgs: config.codexArgs?.length ? config.codexArgs : defaultCodexArgs,
|
|
80
|
+
codexWorkdir: flags.codexWorkdir || config.codexWorkdir || process.cwd(),
|
|
81
|
+
bridgeMode: flags.bridgeMode || config.bridgeMode || "codex-cli",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await installCodexMcpServer(nextConfig, { local: flags.localMcp });
|
|
85
|
+
await smokeCheck();
|
|
86
|
+
|
|
87
|
+
if (!flags.noBridge) {
|
|
88
|
+
await startBridgeInBackground(nextConfig);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log("SkillHub is connected.");
|
|
92
|
+
console.log('In Codex, ask: "Show my SkillHub options."');
|
|
93
|
+
console.log(`Config saved at ${getConfigPath()}`);
|
|
94
|
+
} finally {
|
|
95
|
+
rl?.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function installCodexMcpServer(config, { local = false } = {}) {
|
|
100
|
+
const codexDir = path.join(os.homedir(), ".codex");
|
|
101
|
+
const configPath = path.join(codexDir, "config.toml");
|
|
102
|
+
await mkdir(codexDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
const existing = existsSync(configPath) ? await readFile(configPath, "utf8") : "";
|
|
105
|
+
const withoutSkillhub = removeTomlSection(existing, "mcp_servers.skillhub").trimEnd();
|
|
106
|
+
const command = local ? process.execPath : "npx";
|
|
107
|
+
const args = local
|
|
108
|
+
? [cliPath, "mcp", "serve"]
|
|
109
|
+
: ["-y", packageName, "mcp", "serve"];
|
|
110
|
+
const section = [
|
|
111
|
+
"[mcp_servers.skillhub]",
|
|
112
|
+
`command = ${tomlString(command)}`,
|
|
113
|
+
`args = ${tomlArray(args)}`,
|
|
114
|
+
...(local ? [`cwd = ${tomlString(packageRoot)}`] : []),
|
|
115
|
+
"startup_timeout_sec = 10",
|
|
116
|
+
"tool_timeout_sec = 120",
|
|
117
|
+
"enabled = true",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
const next = [withoutSkillhub, section].filter(Boolean).join("\n\n");
|
|
121
|
+
await writeFile(configPath, `${next}\n`, "utf8");
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function startBridgeInBackground(config) {
|
|
126
|
+
if (!config.convexUrl) {
|
|
127
|
+
throw new Error("A SkillHub connection URL is required before starting the bridge.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configDir = getConfigDir();
|
|
131
|
+
await mkdir(configDir, { recursive: true });
|
|
132
|
+
const logPath = path.join(configDir, "bridge.log");
|
|
133
|
+
const errPath = path.join(configDir, "bridge.err.log");
|
|
134
|
+
const out = await open(logPath, "a");
|
|
135
|
+
const err = await open(errPath, "a");
|
|
136
|
+
const child = spawn(process.execPath, [bridgePath], {
|
|
137
|
+
cwd: packageRoot,
|
|
138
|
+
detached: true,
|
|
139
|
+
env: createBridgeEnv(config),
|
|
140
|
+
stdio: ["ignore", out.fd, err.fd],
|
|
141
|
+
windowsHide: true,
|
|
142
|
+
});
|
|
143
|
+
child.unref();
|
|
144
|
+
|
|
145
|
+
await writeUserConfig({
|
|
146
|
+
...config,
|
|
147
|
+
bridge: {
|
|
148
|
+
...(config.bridge ?? {}),
|
|
149
|
+
pid: child.pid,
|
|
150
|
+
logPath,
|
|
151
|
+
startedAt: Date.now(),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await out.close();
|
|
156
|
+
await err.close();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function smokeCheck() {
|
|
160
|
+
const items = await getCloudCatalogItems({ limit: 1 });
|
|
161
|
+
if (!Array.isArray(items)) {
|
|
162
|
+
throw new Error("SkillHub could not list options.");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function discoverConnectionUrl(workspaceUrl) {
|
|
167
|
+
if (!workspaceUrl) return "";
|
|
168
|
+
if (/\.convex\.(cloud|site)/i.test(workspaceUrl)) return workspaceUrl;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const metadataUrl = new URL("/.well-known/skillhub.json", workspaceUrl);
|
|
172
|
+
const response = await fetch(metadataUrl, { signal: AbortSignal.timeout(5000) });
|
|
173
|
+
if (!response.ok) return "";
|
|
174
|
+
const metadata = await response.json();
|
|
175
|
+
return typeof metadata?.convexUrl === "string" ? metadata.convexUrl.trim() : "";
|
|
176
|
+
} catch {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function maybeInstallCodex({ rl, yes }) {
|
|
182
|
+
const shouldInstall =
|
|
183
|
+
yes || (await askYesNo(rl, "Codex CLI was not found. Install it now with npm?"));
|
|
184
|
+
if (!shouldInstall) {
|
|
185
|
+
throw new Error("Install Codex, then run skillhub connect again.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log("Installing Codex...");
|
|
189
|
+
const result = await spawnCapture({
|
|
190
|
+
command: "npm",
|
|
191
|
+
args: ["install", "-g", "@openai/codex"],
|
|
192
|
+
timeoutMs: 5 * 60 * 1000,
|
|
193
|
+
});
|
|
194
|
+
if (result.code !== 0) {
|
|
195
|
+
throw new Error(formatCommandFailure(result, "npm install -g @openai/codex"));
|
|
196
|
+
}
|
|
197
|
+
console.log("Codex is installed.");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runCodexLogin({ command, rl, yes }) {
|
|
201
|
+
const shouldLogin = yes || (await askYesNo(rl, "Codex is not signed in. Run codex login now?"));
|
|
202
|
+
if (!shouldLogin) {
|
|
203
|
+
throw new Error("Sign in with codex login, then run skillhub connect again.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const login = await spawnInherit({ command, args: ["login"] });
|
|
207
|
+
if (login !== 0) {
|
|
208
|
+
throw new Error("codex login did not complete successfully.");
|
|
209
|
+
}
|
|
210
|
+
console.log("Codex is signed in.");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function checkCommand(command) {
|
|
214
|
+
const result = await spawnCapture({ command, args: ["--version"], timeoutMs: 20_000 });
|
|
215
|
+
if (result.error?.includes("ENOENT")) {
|
|
216
|
+
return { ok: false, missing: true, detail: result.error };
|
|
217
|
+
}
|
|
218
|
+
if (result.code === 0) return { ok: true, detail: result.stdout.trim() };
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
missing: false,
|
|
222
|
+
detail: result.error || result.stderr || result.stdout || `exit ${result.code}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function findConfiguredCodexCommand() {
|
|
227
|
+
const configPath = path.join(os.homedir(), ".codex", "config.toml");
|
|
228
|
+
if (!existsSync(configPath)) return null;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const configText = await readFile(configPath, "utf8");
|
|
232
|
+
const match = configText.match(/^\s*CODEX_CLI_PATH\s*=\s*['"]([^'"]+)['"]/m);
|
|
233
|
+
const command = match?.[1];
|
|
234
|
+
if (!command) return null;
|
|
235
|
+
const result = await checkCommand(command);
|
|
236
|
+
return result.ok ? { ...result, command } : null;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function isCodexSignedIn() {
|
|
243
|
+
const authPath = path.join(os.homedir(), ".codex", "auth.json");
|
|
244
|
+
if (!existsSync(authPath)) return false;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const auth = JSON.parse(await readFile(authPath, "utf8"));
|
|
248
|
+
return Boolean(auth?.tokens || auth?.OPENAI_API_KEY || auth?.account);
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function askRequired(rl, prompt) {
|
|
255
|
+
const value = await askOptional(rl, prompt);
|
|
256
|
+
if (!value) {
|
|
257
|
+
throw new Error("SkillHub needs a workspace URL to connect.");
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function askOptional(rl, prompt) {
|
|
263
|
+
if (!rl) return "";
|
|
264
|
+
const answer = await rl.question(prompt);
|
|
265
|
+
return answer.trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function askYesNo(rl, prompt) {
|
|
269
|
+
if (!rl) return false;
|
|
270
|
+
const answer = await rl.question(`${prompt} [Y/n] `);
|
|
271
|
+
return !/^n(o)?$/i.test(answer.trim());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseFlags(argv) {
|
|
275
|
+
const flags = {
|
|
276
|
+
bridgeMode: "",
|
|
277
|
+
codexCommand: "",
|
|
278
|
+
codexWorkdir: "",
|
|
279
|
+
convexUrl: "",
|
|
280
|
+
noBridge: false,
|
|
281
|
+
localMcp: false,
|
|
282
|
+
workspaceUrl: "",
|
|
283
|
+
yes: false,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
287
|
+
const arg = argv[index];
|
|
288
|
+
if (arg === "--bridge-mode") flags.bridgeMode = argv[++index] ?? "";
|
|
289
|
+
else if (arg === "--codex-command") flags.codexCommand = argv[++index] ?? "";
|
|
290
|
+
else if (arg === "--codex-workdir") flags.codexWorkdir = argv[++index] ?? "";
|
|
291
|
+
else if (arg === "--convex-url") flags.convexUrl = argv[++index] ?? "";
|
|
292
|
+
else if (arg === "--local-mcp") flags.localMcp = true;
|
|
293
|
+
else if (arg === "--no-bridge") flags.noBridge = true;
|
|
294
|
+
else if (arg === "--workspace-url") flags.workspaceUrl = argv[++index] ?? "";
|
|
295
|
+
else if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
296
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return flags;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function removeTomlSection(text, dottedName) {
|
|
303
|
+
const lines = text.split(/\r?\n/);
|
|
304
|
+
const output = [];
|
|
305
|
+
let skipping = false;
|
|
306
|
+
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
const header = line.match(/^\s*\[([^\]]+)]\s*$/);
|
|
309
|
+
if (header) {
|
|
310
|
+
skipping = header[1] === dottedName || header[1].startsWith(`${dottedName}.`);
|
|
311
|
+
}
|
|
312
|
+
if (!skipping) output.push(line);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return output.join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function tomlString(value) {
|
|
319
|
+
return JSON.stringify(String(value).replace(/\\/g, "/"));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function tomlArray(values) {
|
|
323
|
+
return `[${values.map(tomlString).join(", ")}]`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function spawnCapture({ command, args, timeoutMs }) {
|
|
327
|
+
return await new Promise((resolve) => {
|
|
328
|
+
let child;
|
|
329
|
+
const stdout = [];
|
|
330
|
+
const stderr = [];
|
|
331
|
+
let settled = false;
|
|
332
|
+
|
|
333
|
+
const finish = (result) => {
|
|
334
|
+
if (settled) return;
|
|
335
|
+
settled = true;
|
|
336
|
+
resolve({
|
|
337
|
+
stdout: stdout.join(""),
|
|
338
|
+
stderr: stderr.join(""),
|
|
339
|
+
...result,
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
child = spawn(command, args, {
|
|
345
|
+
shell: false,
|
|
346
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
347
|
+
windowsHide: true,
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
finish({ code: null, error: formatError(error) });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const timer = setTimeout(() => {
|
|
355
|
+
child.kill("SIGTERM");
|
|
356
|
+
finish({ code: null, error: `Timed out after ${timeoutMs} ms.` });
|
|
357
|
+
}, timeoutMs);
|
|
358
|
+
|
|
359
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk.toString()));
|
|
360
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk.toString()));
|
|
361
|
+
child.on("error", (error) => {
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
finish({ code: null, error: formatError(error) });
|
|
364
|
+
});
|
|
365
|
+
child.on("close", (code, signal) => {
|
|
366
|
+
clearTimeout(timer);
|
|
367
|
+
finish({ code, signal });
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function spawnInherit({ command, args }) {
|
|
373
|
+
return await new Promise((resolve, reject) => {
|
|
374
|
+
const child = spawn(command, args, {
|
|
375
|
+
shell: false,
|
|
376
|
+
stdio: "inherit",
|
|
377
|
+
windowsHide: false,
|
|
378
|
+
});
|
|
379
|
+
child.on("error", reject);
|
|
380
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function formatCommandFailure(result, command) {
|
|
385
|
+
return [
|
|
386
|
+
`${command} failed`,
|
|
387
|
+
result.code !== null && result.code !== undefined ? `exit=${result.code}` : null,
|
|
388
|
+
result.signal ? `signal=${result.signal}` : null,
|
|
389
|
+
result.stderr ? `stderr=${tail(result.stderr)}` : null,
|
|
390
|
+
result.stdout && !result.stderr ? `stdout=${tail(result.stdout)}` : null,
|
|
391
|
+
result.error ? `error=${result.error}` : null,
|
|
392
|
+
]
|
|
393
|
+
.filter(Boolean)
|
|
394
|
+
.join("; ");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function formatError(error) {
|
|
398
|
+
if (!error) return "Unknown error";
|
|
399
|
+
const parts = [error.message ?? String(error)];
|
|
400
|
+
if (error.code) parts.push(error.code);
|
|
401
|
+
if (error.path) parts.push(error.path);
|
|
402
|
+
return parts.join("; ");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function tail(text) {
|
|
406
|
+
const normalized = text.replace(/\r/g, "").trim();
|
|
407
|
+
return normalized.length <= 700
|
|
408
|
+
? normalized
|
|
409
|
+
: `${normalized.slice(0, 240)}...${normalized.slice(-360)}`;
|
|
410
|
+
}
|