@youdie006/prodex 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +418 -0
- package/dist/banner.d.ts +5 -0
- package/dist/banner.js +47 -0
- package/dist/banner.js.map +1 -0
- package/dist/bundle.d.ts +15 -0
- package/dist/bundle.js +30 -0
- package/dist/bundle.js.map +1 -0
- package/dist/chatgpt-browser.d.ts +119 -0
- package/dist/chatgpt-browser.js +857 -0
- package/dist/chatgpt-browser.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +3502 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.js +277 -0
- package/dist/config.js.map +1 -0
- package/dist/http-mcp.d.ts +20 -0
- package/dist/http-mcp.js +236 -0
- package/dist/http-mcp.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-tools.d.ts +395 -0
- package/dist/mcp-tools.js +167 -0
- package/dist/mcp-tools.js.map +1 -0
- package/dist/mcp.d.ts +37 -0
- package/dist/mcp.js +229 -0
- package/dist/mcp.js.map +1 -0
- package/dist/repo-write.d.ts +47 -0
- package/dist/repo-write.js +427 -0
- package/dist/repo-write.js.map +1 -0
- package/dist/repo.d.ts +27 -0
- package/dist/repo.js +386 -0
- package/dist/repo.js.map +1 -0
- package/dist/safe-file.d.ts +25 -0
- package/dist/safe-file.js +295 -0
- package/dist/safe-file.js.map +1 -0
- package/dist/schema.d.ts +402 -0
- package/dist/schema.js +109 -0
- package/dist/schema.js.map +1 -0
- package/dist/store.d.ts +157 -0
- package/dist/store.js +1402 -0
- package/dist/store.js.map +1 -0
- package/docs/claude.md +130 -0
- package/docs/clients.md +75 -0
- package/docs/http-mcp.md +223 -0
- package/package.json +67 -0
- package/scripts/release-check.mjs +436 -0
- package/scripts/release-pack.mjs +481 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { realpathSync, statSync } from "node:fs";
|
|
4
|
+
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
11
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
12
|
+
import { renderBanner, shouldColorize } from "./banner.js";
|
|
13
|
+
import { buildDryRunBundle } from "./bundle.js";
|
|
14
|
+
import { chatGptVisibilityBlocker, defaultChatGptProfileDir, getChatGptBrowserStatus, normalizeChatGptTargetUrl, openChatGptBrowser, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
15
|
+
import { getTokenExpiryStatus, loadLocalConfig, writeLocalConfig } from "./config.js";
|
|
16
|
+
import { startHttpMcpServer } from "./http-mcp.js";
|
|
17
|
+
import { createMcpToolHandlers } from "./mcp-tools.js";
|
|
18
|
+
import { runMcpServer } from "./mcp.js";
|
|
19
|
+
import { readVerifiedUtf8File, writeVerifiedUtf8File } from "./safe-file.js";
|
|
20
|
+
import { ReceiptKindSchema, TaskStatusSchema } from "./schema.js";
|
|
21
|
+
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
const requirePackageJson = createRequire(import.meta.url);
|
|
24
|
+
const packageJson = requirePackageJson("../package.json");
|
|
25
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
const CLI_VERSION = packageJson.version ?? "0.0.0";
|
|
27
|
+
const RESERVED_PACKAGE_NAMES = new Set(["node_modules", "favicon.ico"]);
|
|
28
|
+
const PRO_BROWSER_SMOKE_TOKEN = "PRODEX_PRO_SMOKE_OK";
|
|
29
|
+
const TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING = "Token-bearing MCP URL authorizes all enabled bridge tools, including repo_read_file, repo_search, repo_write_file_dry_run, repo_write_file_apply, and repo_stage_reviewed_paths. Paste it only into your own trusted private MCP client.";
|
|
30
|
+
const TOP_LEVEL_COMMANDS = [
|
|
31
|
+
"help",
|
|
32
|
+
"version",
|
|
33
|
+
"init",
|
|
34
|
+
"setup",
|
|
35
|
+
"start",
|
|
36
|
+
"status",
|
|
37
|
+
"tunnel",
|
|
38
|
+
"doctor",
|
|
39
|
+
"onboard",
|
|
40
|
+
"project",
|
|
41
|
+
"claude",
|
|
42
|
+
"tasks",
|
|
43
|
+
"results",
|
|
44
|
+
"receipts",
|
|
45
|
+
"sessions",
|
|
46
|
+
"pro",
|
|
47
|
+
"release",
|
|
48
|
+
"mcp"
|
|
49
|
+
];
|
|
50
|
+
const DOCTOR_REQUIRED_MCP_TOOLS = [
|
|
51
|
+
"bridge_create_task",
|
|
52
|
+
"bridge_list_tasks",
|
|
53
|
+
"bridge_get_task",
|
|
54
|
+
"bridge_claim_task",
|
|
55
|
+
"bridge_complete_task",
|
|
56
|
+
"bridge_block_task",
|
|
57
|
+
"bridge_list_results",
|
|
58
|
+
"bridge_fetch_result",
|
|
59
|
+
"bridge_fetch_result_artifact",
|
|
60
|
+
"bridge_list_receipts",
|
|
61
|
+
"bridge_get_receipt",
|
|
62
|
+
"bridge_list_sessions",
|
|
63
|
+
"bridge_get_session",
|
|
64
|
+
"repo_read_file",
|
|
65
|
+
"repo_search",
|
|
66
|
+
"repo_write_file_dry_run",
|
|
67
|
+
"repo_write_file_apply",
|
|
68
|
+
"repo_stage_reviewed_paths"
|
|
69
|
+
];
|
|
70
|
+
export async function runCli(args, io = defaultIo()) {
|
|
71
|
+
const [command, ...rest] = args;
|
|
72
|
+
const store = new BridgeStore(io.cwd);
|
|
73
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
74
|
+
io.stdout(CLI_VERSION);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
78
|
+
if (shouldColorize())
|
|
79
|
+
io.stdout(renderBanner({ color: true }));
|
|
80
|
+
printHelp(io.stdout);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
if (command === "init") {
|
|
84
|
+
if (printHelpIfRequested(rest, "init", io.stdout, printInitHelp, { valueFlags: ["--cwd"] }))
|
|
85
|
+
return 0;
|
|
86
|
+
assertOnlyOptions(rest, "init", ["--cwd"]);
|
|
87
|
+
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
88
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
89
|
+
await targetStore.ensure();
|
|
90
|
+
await ensureBridgeGitignore(targetCwd);
|
|
91
|
+
io.stdout("Initialized .bridge receipt ledger.");
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
if (command === "setup") {
|
|
95
|
+
if (printHelpIfRequested(rest, "setup", io.stdout, printSetupHelp, { valueFlags: ["--cwd", "--host", "--port", "--token", "--token-ttl-hours"] }))
|
|
96
|
+
return 0;
|
|
97
|
+
assertOnlyOptions(rest, "setup", ["--cwd", "--host", "--port", "--token", "--token-ttl-hours"]);
|
|
98
|
+
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
99
|
+
const config = await writeLocalConfig(targetCwd, {
|
|
100
|
+
host: readFlag(rest, "--host") ?? "127.0.0.1",
|
|
101
|
+
port: readPortFlag(rest, "--port") ?? 8787,
|
|
102
|
+
token: readFlag(rest, "--token"),
|
|
103
|
+
tokenTtlHours: readPositiveNumberFlag(rest, "--token-ttl-hours")
|
|
104
|
+
});
|
|
105
|
+
io.stdout("Saved local ChatGPT Developer Mode MCP profile.");
|
|
106
|
+
io.stdout(`Server URL: ${redactServerUrl(config.server_url)}`);
|
|
107
|
+
io.stdout(formatTokenExpiryLine(config));
|
|
108
|
+
io.stdout("Full URL is stored in .bridge/config.local.json.");
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (command === "start") {
|
|
112
|
+
if (printHelpIfRequested(rest, "start", io.stdout, printStartHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
113
|
+
return 0;
|
|
114
|
+
assertOnlyOptions(rest, "start", ["--cwd", "--source-cli"]);
|
|
115
|
+
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
116
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
117
|
+
const setupHintCwd = readFlag(rest, "--cwd") ? targetCwd : undefined;
|
|
118
|
+
const config = await loadLocalConfigForCommand(targetCwd, "start", sourceCli, setupHintCwd);
|
|
119
|
+
assertTokenNotExpiredForCommand(config, sourceCli, setupHintCwd);
|
|
120
|
+
const running = await startHttpMcpServer({
|
|
121
|
+
cwd: targetCwd,
|
|
122
|
+
host: config.host,
|
|
123
|
+
port: config.port,
|
|
124
|
+
token: config.token,
|
|
125
|
+
tokenExpiresAt: config.token_expires_at
|
|
126
|
+
});
|
|
127
|
+
io.stdout(`prodex HTTP MCP listening on ${redactServerUrl(running.mcp_url)}`);
|
|
128
|
+
io.stdout(formatTokenExpiryLine(config));
|
|
129
|
+
await waitForShutdown(async () => running.close());
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (command === "status") {
|
|
133
|
+
if (printHelpIfRequested(rest, "status", io.stdout, printStatusHelp, {
|
|
134
|
+
valueFlags: ["--cwd", "--source-cli"],
|
|
135
|
+
booleanFlags: ["--show-token", "--url-only", "--unsafe-show-non-expiring-token"]
|
|
136
|
+
})) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
assertOnlyOptions(rest, "status", ["--cwd", "--source-cli"], ["--show-token", "--url-only", "--unsafe-show-non-expiring-token"]);
|
|
140
|
+
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
141
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
142
|
+
const setupHintCwd = readFlag(rest, "--cwd") ? targetCwd : undefined;
|
|
143
|
+
const config = await loadLocalConfigForCommand(targetCwd, "status", sourceCli, setupHintCwd);
|
|
144
|
+
const showToken = rest.includes("--show-token");
|
|
145
|
+
const allowNonExpiringTokenReveal = rest.includes("--unsafe-show-non-expiring-token");
|
|
146
|
+
const tokenStatus = getTokenExpiryStatus(config);
|
|
147
|
+
if (showToken && tokenStatus.status === "non_expiring" && !allowNonExpiringTokenReveal) {
|
|
148
|
+
throw new Error(sourceAwareSetupMessage("status --show-token requires a token with expiry. Run `prodex setup --token-ttl-hours <hours>` first, or pass --unsafe-show-non-expiring-token for local-only debugging.", sourceCli, { cwd: setupHintCwd }));
|
|
149
|
+
}
|
|
150
|
+
if (showToken && tokenStatus.status === "expired") {
|
|
151
|
+
throw new Error(sourceAwareSetupMessage(`token expired at ${tokenStatus.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\`.`, sourceCli, {
|
|
152
|
+
cwd: setupHintCwd
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
const nonExpiringRevealWarning = showToken && allowNonExpiringTokenReveal && tokenStatus.status === "non_expiring"
|
|
156
|
+
? sourceAwareSetupMessage("Showing a non-expiring token. Keep this local-only and rotate it with `prodex setup --token-ttl-hours <hours>` before any tunnel or ChatGPT Project use.", sourceCli, { cwd: setupHintCwd })
|
|
157
|
+
: undefined;
|
|
158
|
+
const serverUrl = formatServerUrlForOutput(config.server_url, { showToken });
|
|
159
|
+
if (rest.includes("--url-only")) {
|
|
160
|
+
if (showToken)
|
|
161
|
+
io.stderr(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
162
|
+
if (nonExpiringRevealWarning)
|
|
163
|
+
io.stderr(nonExpiringRevealWarning);
|
|
164
|
+
io.stdout(serverUrl);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
const warnings = tokenStatus.warning ? [sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })] : [];
|
|
168
|
+
if (showToken)
|
|
169
|
+
warnings.push(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
170
|
+
if (nonExpiringRevealWarning)
|
|
171
|
+
warnings.push(nonExpiringRevealWarning);
|
|
172
|
+
io.stdout(JSON.stringify({
|
|
173
|
+
server_url: serverUrl,
|
|
174
|
+
config_path: ".bridge/config.local.json",
|
|
175
|
+
token_status: tokenStatus.status,
|
|
176
|
+
token_expires_at: tokenStatus.token_expires_at ?? null,
|
|
177
|
+
warnings
|
|
178
|
+
}, null, 2));
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
if (command === "tunnel") {
|
|
182
|
+
const [subcommand, ...tunnelArgs] = rest;
|
|
183
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
184
|
+
assertNoExtraArgs(tunnelArgs, "tunnel help", 0);
|
|
185
|
+
printTunnelHelp(io.stdout);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
if (subcommand !== "url")
|
|
189
|
+
throw unknownSubcommandError("tunnel", subcommand, ["url"]);
|
|
190
|
+
if (printHelpIfRequested(tunnelArgs, "tunnel url", io.stdout, printTunnelUrlHelp, {
|
|
191
|
+
valueFlags: ["--cwd", "--public-url", "--source-cli"],
|
|
192
|
+
booleanFlags: ["--show-token", "--url-only"]
|
|
193
|
+
})) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
assertOnlyOptions(tunnelArgs, "tunnel url", ["--cwd", "--public-url", "--source-cli"], ["--show-token", "--url-only"]);
|
|
197
|
+
const targetCwd = resolveCwdFlag(io.cwd, tunnelArgs);
|
|
198
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, tunnelArgs, "--source-cli");
|
|
199
|
+
const setupHintCwd = readFlag(tunnelArgs, "--cwd") ? targetCwd : undefined;
|
|
200
|
+
const publicUrl = readFlag(tunnelArgs, "--public-url");
|
|
201
|
+
if (!publicUrl)
|
|
202
|
+
throw new Error("tunnel url requires --public-url <https-url>");
|
|
203
|
+
parseTunnelPublicUrl(publicUrl);
|
|
204
|
+
const config = await loadLocalConfigForCommand(targetCwd, "tunnel url", sourceCli, setupHintCwd);
|
|
205
|
+
const tokenStatus = getTokenExpiryStatus(config);
|
|
206
|
+
if (tokenStatus.status === "non_expiring") {
|
|
207
|
+
throw new Error(sourceAwareSetupMessage("tunnel url requires a short-lived token. Run `prodex setup --token-ttl-hours <hours>` first.", sourceCli, {
|
|
208
|
+
cwd: setupHintCwd
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
if (tokenStatus.status === "expired") {
|
|
212
|
+
throw new Error(sourceAwareSetupMessage(`token expired at ${tokenStatus.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\`.`, sourceCli, {
|
|
213
|
+
cwd: setupHintCwd
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
const mcpUrl = makeTunnelMcpUrl(publicUrl, config.token);
|
|
217
|
+
const showToken = tunnelArgs.includes("--show-token");
|
|
218
|
+
const outputUrl = showToken ? mcpUrl : redactServerUrl(mcpUrl);
|
|
219
|
+
if (tunnelArgs.includes("--url-only")) {
|
|
220
|
+
if (showToken)
|
|
221
|
+
io.stderr(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
222
|
+
io.stdout(outputUrl);
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
const warnings = [
|
|
226
|
+
"This command does not create a tunnel. Keep `prodex start` running behind your own tunnel.",
|
|
227
|
+
"Only paste the token-bearing URL into a trusted private MCP client."
|
|
228
|
+
];
|
|
229
|
+
if (showToken)
|
|
230
|
+
warnings.push(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
231
|
+
io.stdout(JSON.stringify({
|
|
232
|
+
mcp_url: outputUrl,
|
|
233
|
+
token_status: tokenStatus.status,
|
|
234
|
+
token_expires_at: tokenStatus.token_expires_at,
|
|
235
|
+
warnings
|
|
236
|
+
}, null, 2));
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
if (command === "doctor") {
|
|
240
|
+
if (printHelpIfRequested(rest, "doctor", io.stdout, printDoctorHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
241
|
+
return 0;
|
|
242
|
+
assertOnlyOptions(rest, "doctor", ["--cwd", "--source-cli"]);
|
|
243
|
+
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
244
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
245
|
+
return runDoctor(new BridgeStore(targetCwd), { ...io, cwd: targetCwd }, sourceCli, readFlag(rest, "--cwd") ? targetCwd : undefined);
|
|
246
|
+
}
|
|
247
|
+
if (command === "release") {
|
|
248
|
+
const [subcommand, ...releaseArgs] = rest;
|
|
249
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
250
|
+
assertNoExtraArgs(releaseArgs, "release help", 0);
|
|
251
|
+
printReleaseHelp(io.stdout);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
if (subcommand === "status") {
|
|
255
|
+
if (printHelpIfRequested(releaseArgs, "release status", io.stdout, printReleaseHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
256
|
+
return 0;
|
|
257
|
+
assertOnlyOptions(releaseArgs, "release status", ["--cwd", "--source-cli"]);
|
|
258
|
+
const targetCwd = resolveCwdFlag(io.cwd, releaseArgs);
|
|
259
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, releaseArgs, "--source-cli");
|
|
260
|
+
io.stdout(await formatReleaseStatus(targetCwd, sourceCli, readFlag(releaseArgs, "--cwd") ? targetCwd : undefined));
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
if (subcommand === "pack") {
|
|
264
|
+
if (printHelpIfRequested(releaseArgs, "release pack", io.stdout, printReleaseHelp, {
|
|
265
|
+
valueFlags: ["--cwd", "--pack-destination", "--source-cli"],
|
|
266
|
+
booleanFlags: ["--keep-workdir"]
|
|
267
|
+
})) {
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
assertOnlyOptions(releaseArgs, "release pack", ["--cwd", "--pack-destination", "--source-cli"], ["--keep-workdir"]);
|
|
271
|
+
const targetCwd = resolveCwdFlag(io.cwd, releaseArgs);
|
|
272
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, releaseArgs, "--source-cli");
|
|
273
|
+
const packDestination = readFlag(releaseArgs, "--pack-destination");
|
|
274
|
+
if (!packDestination)
|
|
275
|
+
throw new Error("release pack requires --pack-destination <dir>");
|
|
276
|
+
await runReleasePackCommand({
|
|
277
|
+
cwd: io.cwd,
|
|
278
|
+
packageRoot: targetCwd,
|
|
279
|
+
packDestination: path.resolve(io.cwd, packDestination),
|
|
280
|
+
keepWorkdir: releaseArgs.includes("--keep-workdir"),
|
|
281
|
+
sourceCli,
|
|
282
|
+
releaseStatusCwd: readFlag(releaseArgs, "--cwd") ? targetCwd : undefined,
|
|
283
|
+
stdout: io.stdout,
|
|
284
|
+
stderr: io.stderr
|
|
285
|
+
});
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
throw unknownSubcommandError("release", subcommand, ["status", "pack"]);
|
|
289
|
+
}
|
|
290
|
+
if (command === "onboard") {
|
|
291
|
+
if (printHelpIfRequested(rest, "onboard", io.stdout, printOnboardHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
292
|
+
return 0;
|
|
293
|
+
assertOnlyOptions(rest, "onboard", ["--cwd", "--source-cli"]);
|
|
294
|
+
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
295
|
+
if (shouldColorize())
|
|
296
|
+
io.stdout(renderBanner({ color: true }));
|
|
297
|
+
io.stdout(formatOnboardingGuide(targetCwd, await hasOnboardingReadme(targetCwd), resolveOptionalFileFlag(io.cwd, rest, "--source-cli")));
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
if (command === "project") {
|
|
301
|
+
const [subcommand, ...projectArgs] = rest;
|
|
302
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
303
|
+
assertNoExtraArgs(projectArgs, "project help", 0);
|
|
304
|
+
printProjectHelp(io.stdout);
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (subcommand !== "prompt")
|
|
308
|
+
throw unknownSubcommandError("project", subcommand, ["prompt"]);
|
|
309
|
+
if (printHelpIfRequested(projectArgs, "project prompt", io.stdout, printProjectHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
310
|
+
return 0;
|
|
311
|
+
assertOnlyOptions(projectArgs, "project prompt", ["--cwd", "--source-cli"]);
|
|
312
|
+
io.stdout(formatProjectVerificationPrompt(resolveCwdFlag(io.cwd, projectArgs), resolveOptionalFileFlag(io.cwd, projectArgs, "--source-cli")));
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
if (command === "claude") {
|
|
316
|
+
const [subcommand, ...claudeArgs] = rest;
|
|
317
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
318
|
+
assertNoExtraArgs(claudeArgs, "claude help", 0);
|
|
319
|
+
printClaudeHelp(io.stdout);
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
if (subcommand === "prompt") {
|
|
323
|
+
if (printHelpIfRequested(claudeArgs, "claude prompt", io.stdout, printClaudeHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
324
|
+
return 0;
|
|
325
|
+
assertOnlyOptions(claudeArgs, "claude prompt", ["--cwd", "--source-cli"]);
|
|
326
|
+
io.stdout(formatClaudeVerificationPrompt(resolveCwdFlag(io.cwd, claudeArgs), resolveOptionalFileFlag(io.cwd, claudeArgs, "--source-cli")));
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
if (subcommand === "config") {
|
|
330
|
+
if (printHelpIfRequested(claudeArgs, "claude config", io.stdout, printClaudeHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
331
|
+
return 0;
|
|
332
|
+
assertOnlyOptions(claudeArgs, "claude config", ["--cwd", "--source-cli"]);
|
|
333
|
+
io.stdout(formatClaudeConfig(resolveCwdFlag(io.cwd, claudeArgs), resolveOptionalFileFlag(io.cwd, claudeArgs, "--source-cli")));
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
throw unknownSubcommandError("claude", subcommand, ["prompt", "config"]);
|
|
337
|
+
}
|
|
338
|
+
if (command === "chatgpt") {
|
|
339
|
+
const [subcommand, ...chatgptArgs] = rest;
|
|
340
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
341
|
+
throw legacyChatGptNamespaceError();
|
|
342
|
+
}
|
|
343
|
+
if (subcommand === "open") {
|
|
344
|
+
assertOnlyOptions(chatgptArgs, "chatgpt open", ["--port", "--profile-dir", "--url"]);
|
|
345
|
+
const opened = openChatGptBrowser({
|
|
346
|
+
port: readPortFlag(chatgptArgs, "--port") ?? 9333,
|
|
347
|
+
profileDir: readFlag(chatgptArgs, "--profile-dir"),
|
|
348
|
+
url: readChatGptBrowserUrlFlag(chatgptArgs)
|
|
349
|
+
});
|
|
350
|
+
await assertBrowserLaunchStayedAlive(opened);
|
|
351
|
+
io.stdout(`Opened ChatGPT browser via ${opened.command}.`);
|
|
352
|
+
io.stdout(`Profile: ${opened.profileDir}`);
|
|
353
|
+
io.stdout(`Debug: http://127.0.0.1:${opened.port}`);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
if (subcommand === "status") {
|
|
357
|
+
assertOnlyOptions(chatgptArgs, "chatgpt status", ["--port"]);
|
|
358
|
+
const status = await getChatGptBrowserStatus({ port: readPortFlag(chatgptArgs, "--port") ?? 9333 });
|
|
359
|
+
io.stdout(JSON.stringify(status, null, 2));
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
if (subcommand === "smoke") {
|
|
363
|
+
assertOnlyOptions(chatgptArgs, "chatgpt smoke", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
|
|
364
|
+
const targetCwd = resolveCwdFlag(io.cwd, chatgptArgs);
|
|
365
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
366
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, chatgptArgs, "--source-cli");
|
|
367
|
+
const port = readPortFlag(chatgptArgs, "--port") ?? 9333;
|
|
368
|
+
const timeoutMs = readPositiveNumberFlag(chatgptArgs, "--timeout-ms") ?? 90000;
|
|
369
|
+
const commandOptions = {
|
|
370
|
+
...(readFlag(chatgptArgs, "--cwd") ? { cwd: targetCwd } : {}),
|
|
371
|
+
...(readFlag(chatgptArgs, "--port") ? { port } : {})
|
|
372
|
+
};
|
|
373
|
+
const smokePrompt = `This is a one-time prodex smoke test. Reply exactly: ${PRO_BROWSER_SMOKE_TOKEN}`;
|
|
374
|
+
const recordBlockedSmoke = async (summary, blocker, thread) => {
|
|
375
|
+
const bundle = await buildDryRunBundle(targetCwd, { prompt: smokePrompt, files: [] });
|
|
376
|
+
const task = await targetStore.createTask({
|
|
377
|
+
source: "codex",
|
|
378
|
+
title: "GPT Pro smoke",
|
|
379
|
+
prompt: bundle.text,
|
|
380
|
+
repo_id: "default",
|
|
381
|
+
provenance: {
|
|
382
|
+
adapter: "chatgpt-control",
|
|
383
|
+
session_id: bundle.id,
|
|
384
|
+
thread,
|
|
385
|
+
warnings: []
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
await targetStore.claimTask(task.id, "chatgpt-pro");
|
|
389
|
+
await targetStore.completeTask(task.id, {
|
|
390
|
+
status: "blocked",
|
|
391
|
+
summary,
|
|
392
|
+
commands: ["visible ChatGPT browser smoke"],
|
|
393
|
+
blocker
|
|
394
|
+
});
|
|
395
|
+
await writeSessionBestEffort(targetStore, {
|
|
396
|
+
id: bundle.id,
|
|
397
|
+
direction: "codex_to_chatgpt",
|
|
398
|
+
backend: "chatgpt-control",
|
|
399
|
+
task_id: task.id,
|
|
400
|
+
thread,
|
|
401
|
+
status: "blocked",
|
|
402
|
+
blocker,
|
|
403
|
+
warnings: []
|
|
404
|
+
}, io);
|
|
405
|
+
return task.id;
|
|
406
|
+
};
|
|
407
|
+
let result;
|
|
408
|
+
try {
|
|
409
|
+
result = await sendChatGptPrompt({
|
|
410
|
+
port,
|
|
411
|
+
prompt: smokePrompt,
|
|
412
|
+
timeoutMs
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, commandOptions);
|
|
417
|
+
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
418
|
+
let taskId;
|
|
419
|
+
try {
|
|
420
|
+
taskId = await recordBlockedSmoke(message, blocker);
|
|
421
|
+
}
|
|
422
|
+
catch (recordError) {
|
|
423
|
+
throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
|
|
424
|
+
}
|
|
425
|
+
throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
|
|
426
|
+
}
|
|
427
|
+
if (result.answer.trim() !== PRO_BROWSER_SMOKE_TOKEN) {
|
|
428
|
+
const message = `Pro browser smoke returned an unexpected answer. Expected exactly ${PRO_BROWSER_SMOKE_TOKEN}. Actual: ${firstLine(result.answer)}`;
|
|
429
|
+
const blocker = {
|
|
430
|
+
code: "smoke_token_mismatch",
|
|
431
|
+
message,
|
|
432
|
+
retryable: true,
|
|
433
|
+
next_step: `Retry \`${formatBrowserSmokeCommand(sourceCli, commandOptions)}\` after selecting the intended Pro model, or inspect the visible ChatGPT answer.`
|
|
434
|
+
};
|
|
435
|
+
let taskId;
|
|
436
|
+
try {
|
|
437
|
+
taskId = await recordBlockedSmoke(message, blocker, result.url);
|
|
438
|
+
}
|
|
439
|
+
catch (recordError) {
|
|
440
|
+
throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
|
|
441
|
+
}
|
|
442
|
+
throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
|
|
443
|
+
}
|
|
444
|
+
io.stdout(JSON.stringify(result, null, 2));
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
throw legacyChatGptNamespaceError(subcommand);
|
|
448
|
+
}
|
|
449
|
+
if (command === "tasks") {
|
|
450
|
+
const [subcommand, ...taskArgs] = rest;
|
|
451
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
452
|
+
assertNoExtraArgs(taskArgs, "tasks help", 0);
|
|
453
|
+
printTasksHelp(io.stdout);
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
if (subcommand === "create") {
|
|
457
|
+
if (printHelpIfRequested(taskArgs, "tasks create", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--title", "--prompt", "--repo-id", "--file"] }))
|
|
458
|
+
return 0;
|
|
459
|
+
assertOnlyOptions(taskArgs, "tasks create", ["--cwd", "--title", "--prompt", "--repo-id", "--file"]);
|
|
460
|
+
const title = readFlag(taskArgs, "--title");
|
|
461
|
+
const prompt = readFlag(taskArgs, "--prompt");
|
|
462
|
+
if (!title || !prompt)
|
|
463
|
+
throw new Error("tasks create requires --title and --prompt");
|
|
464
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
465
|
+
const task = await targetStore.createTask({
|
|
466
|
+
source: "codex",
|
|
467
|
+
title,
|
|
468
|
+
prompt,
|
|
469
|
+
repo_id: readFlag(taskArgs, "--repo-id") ?? "default",
|
|
470
|
+
files: readRepeatedFlag(taskArgs, "--file").map((file) => ({ path: file, role: "context" })),
|
|
471
|
+
provenance: { adapter: "cli", warnings: [] }
|
|
472
|
+
});
|
|
473
|
+
io.stdout(`${task.id}\t${task.status}\t${task.title}`);
|
|
474
|
+
return 0;
|
|
475
|
+
}
|
|
476
|
+
if (subcommand === "list") {
|
|
477
|
+
if (printHelpIfRequested(taskArgs, "tasks list", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--status"] }))
|
|
478
|
+
return 0;
|
|
479
|
+
assertOnlyOptions(taskArgs, "tasks list", ["--cwd", "--status"]);
|
|
480
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
481
|
+
const status = readTaskStatusFlag(taskArgs);
|
|
482
|
+
const tasks = await listTasksForInspection(targetStore, status);
|
|
483
|
+
for (const task of tasks) {
|
|
484
|
+
io.stdout(`${task.id}\t${task.status}\t${task.title}`);
|
|
485
|
+
}
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
if (subcommand === "show") {
|
|
489
|
+
if (printHelpIfRequested(taskArgs, "tasks show", io.stdout, printTasksHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
490
|
+
return 0;
|
|
491
|
+
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks show", 1, ["--cwd"]);
|
|
492
|
+
if (!taskId)
|
|
493
|
+
throw new Error("tasks show requires <task-id|latest>");
|
|
494
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
495
|
+
const task = taskId === "latest" ? await latestTask(targetStore, { readOnly: true }) : await targetStore.getTaskReadOnly(taskId);
|
|
496
|
+
if (!task)
|
|
497
|
+
throw new Error(taskId === "latest" ? "No tasks found" : `Task not found: ${taskId}`);
|
|
498
|
+
io.stdout(JSON.stringify(task, null, 2));
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
if (subcommand === "claim") {
|
|
502
|
+
if (printHelpIfRequested(taskArgs, "tasks claim", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--by"], maxPositionals: 1 }))
|
|
503
|
+
return 0;
|
|
504
|
+
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks claim", 1, ["--cwd", "--by"]);
|
|
505
|
+
if (!taskId)
|
|
506
|
+
throw new Error("tasks claim requires <task-id>");
|
|
507
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
508
|
+
const task = await targetStore.claimTask(taskId, readFlag(taskArgs, "--by") ?? "codex");
|
|
509
|
+
io.stdout(`${task.id}\t${task.status}\t${task.claimed_by ?? ""}`);
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
if (subcommand === "complete") {
|
|
513
|
+
if (printHelpIfRequested(taskArgs, "tasks complete", io.stdout, printTasksHelp, {
|
|
514
|
+
valueFlags: ["--cwd", "--summary", "--command", "--artifact"],
|
|
515
|
+
maxPositionals: 1
|
|
516
|
+
})) {
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks complete", 1, ["--cwd", "--summary", "--command", "--artifact"]);
|
|
520
|
+
if (!taskId)
|
|
521
|
+
throw new Error("tasks complete requires <task-id> --summary");
|
|
522
|
+
const summary = readFlag(taskArgs, "--summary");
|
|
523
|
+
if (!summary)
|
|
524
|
+
throw new Error("tasks complete requires <task-id> --summary");
|
|
525
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
526
|
+
const result = await targetStore.completeTask(taskId, {
|
|
527
|
+
status: "done",
|
|
528
|
+
summary,
|
|
529
|
+
commands: readRepeatedFlag(taskArgs, "--command"),
|
|
530
|
+
artifacts: await writeTaskCompleteArtifacts(targetStore, readRepeatedFlag(taskArgs, "--artifact"))
|
|
531
|
+
});
|
|
532
|
+
io.stdout(`${result.task_id}\t${result.status}\t${result.summary}`);
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
if (subcommand === "block") {
|
|
536
|
+
if (printHelpIfRequested(taskArgs, "tasks block", io.stdout, printTasksHelp, {
|
|
537
|
+
valueFlags: ["--cwd", "--summary", "--code", "--next-step", "--command"],
|
|
538
|
+
booleanFlags: ["--retryable"],
|
|
539
|
+
maxPositionals: 1
|
|
540
|
+
})) {
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks block", 1, ["--cwd", "--summary", "--code", "--next-step", "--command"], ["--retryable"]);
|
|
544
|
+
if (!taskId)
|
|
545
|
+
throw new Error("tasks block requires <task-id> --summary");
|
|
546
|
+
const summary = readFlag(taskArgs, "--summary");
|
|
547
|
+
if (!summary)
|
|
548
|
+
throw new Error("tasks block requires <task-id> --summary");
|
|
549
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
550
|
+
const result = await targetStore.completeTask(taskId, {
|
|
551
|
+
status: "blocked",
|
|
552
|
+
summary,
|
|
553
|
+
blocker: {
|
|
554
|
+
code: readFlag(taskArgs, "--code") ?? "manual_blocker",
|
|
555
|
+
message: summary,
|
|
556
|
+
retryable: taskArgs.includes("--retryable"),
|
|
557
|
+
next_step: readFlag(taskArgs, "--next-step")
|
|
558
|
+
},
|
|
559
|
+
commands: readRepeatedFlag(taskArgs, "--command")
|
|
560
|
+
});
|
|
561
|
+
io.stdout(`${result.task_id}\t${result.status}\t${result.summary}`);
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
throw unknownSubcommandError("tasks", subcommand, ["create", "list", "show", "claim", "complete", "block"]);
|
|
565
|
+
}
|
|
566
|
+
if (command === "results") {
|
|
567
|
+
const [subcommand, ...resultArgs] = rest;
|
|
568
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
569
|
+
assertNoExtraArgs(resultArgs, "results help", 0);
|
|
570
|
+
printResultsHelp(io.stdout);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
if (subcommand === "show") {
|
|
574
|
+
if (printHelpIfRequested(resultArgs, "results show", io.stdout, printResultsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
575
|
+
return 0;
|
|
576
|
+
const [taskId] = readPositionalsWithOptions(resultArgs, "results show", 1, ["--cwd"]);
|
|
577
|
+
if (!taskId)
|
|
578
|
+
throw new Error("results show requires <task-id|latest>");
|
|
579
|
+
const targetCwd = resolveCwdFlag(io.cwd, resultArgs);
|
|
580
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
581
|
+
const resultOptions = { cwd: readFlag(resultArgs, "--cwd") ? targetCwd : undefined };
|
|
582
|
+
try {
|
|
583
|
+
const resolvedTaskId = taskId === "latest" ? await latestResultTaskId(targetStore, { readOnly: true }) : taskId;
|
|
584
|
+
io.stdout(JSON.stringify(await targetStore.getFinalizedResultReadOnly(resolvedTaskId), null, 2));
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
throw sourceAwareResultError(error, undefined, resultOptions);
|
|
588
|
+
}
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
if (subcommand === "artifact") {
|
|
592
|
+
if (printHelpIfRequested(resultArgs, "results artifact", io.stdout, printResultsHelp, { valueFlags: ["--cwd"], maxPositionals: 2 }))
|
|
593
|
+
return 0;
|
|
594
|
+
const [taskId, artifactPath] = readPositionalsWithOptions(resultArgs, "results artifact", 2, ["--cwd"]);
|
|
595
|
+
if (!taskId)
|
|
596
|
+
throw new Error("results artifact requires <task-id> [artifact-path]");
|
|
597
|
+
const targetCwd = resolveCwdFlag(io.cwd, resultArgs);
|
|
598
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
599
|
+
const resultOptions = { cwd: readFlag(resultArgs, "--cwd") ? targetCwd : undefined };
|
|
600
|
+
try {
|
|
601
|
+
const resolvedTaskId = taskId === "latest" ? await latestResultTaskId(targetStore, { readOnly: true }) : taskId;
|
|
602
|
+
const artifact = await targetStore.readFinalizedResultArtifactText(resolvedTaskId, artifactPath);
|
|
603
|
+
io.stdout(artifact.content);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
throw sourceAwareResultError(error, undefined, resultOptions);
|
|
607
|
+
}
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
610
|
+
if (subcommand === "reseal") {
|
|
611
|
+
if (printHelpIfRequested(resultArgs, "results reseal", io.stdout, printResultsHelp, {
|
|
612
|
+
valueFlags: ["--cwd"],
|
|
613
|
+
booleanFlags: ["--confirm-current-result"],
|
|
614
|
+
maxPositionals: 1
|
|
615
|
+
})) {
|
|
616
|
+
return 0;
|
|
617
|
+
}
|
|
618
|
+
const [taskId] = readPositionalsWithOptions(resultArgs, "results reseal", 1, ["--cwd"], ["--confirm-current-result"]);
|
|
619
|
+
if (!taskId)
|
|
620
|
+
throw new Error("results reseal requires <task-id|latest> --confirm-current-result");
|
|
621
|
+
if (!resultArgs.includes("--confirm-current-result")) {
|
|
622
|
+
throw new Error("results reseal requires --confirm-current-result after you review the current .bridge/results/<task-id>.json payload locally.");
|
|
623
|
+
}
|
|
624
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, resultArgs));
|
|
625
|
+
const resolvedTaskId = taskId === "latest" ? await latestRawResultTaskId(targetStore) : taskId;
|
|
626
|
+
const resealed = await targetStore.resealResult(resolvedTaskId);
|
|
627
|
+
io.stdout(`${resealed.result.task_id}\tresealed\t${resealed.receipt.id}\tresult_sha256=${resealed.receipt.metadata.result_sha256}`);
|
|
628
|
+
return 0;
|
|
629
|
+
}
|
|
630
|
+
throw unknownSubcommandError("results", subcommand, ["show", "artifact", "reseal"]);
|
|
631
|
+
}
|
|
632
|
+
if (command === "receipts") {
|
|
633
|
+
const [subcommand, ...receiptArgs] = rest;
|
|
634
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
635
|
+
assertNoExtraArgs(receiptArgs, "receipts help", 0);
|
|
636
|
+
printReceiptsHelp(io.stdout);
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
if (subcommand === "list") {
|
|
640
|
+
if (printHelpIfRequested(receiptArgs, "receipts list", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd", "--kind", "--task-id"] }))
|
|
641
|
+
return 0;
|
|
642
|
+
assertOnlyOptions(receiptArgs, "receipts list", ["--cwd", "--kind", "--task-id"]);
|
|
643
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
644
|
+
const receipts = await listReceiptsForInspection(targetStore, {
|
|
645
|
+
kind: readReceiptKindFlag(receiptArgs),
|
|
646
|
+
task_id: readFlag(receiptArgs, "--task-id")
|
|
647
|
+
});
|
|
648
|
+
for (const receipt of receipts) {
|
|
649
|
+
io.stdout(`${receipt.id}\t${receipt.kind}\t${receipt.summary}${receiptInspectionListSuffix(receipt)}`);
|
|
650
|
+
}
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
if (subcommand === "show") {
|
|
654
|
+
if (printHelpIfRequested(receiptArgs, "receipts show", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
655
|
+
return 0;
|
|
656
|
+
const [receiptId] = readPositionalsWithOptions(receiptArgs, "receipts show", 1, ["--cwd"]);
|
|
657
|
+
if (!receiptId)
|
|
658
|
+
throw new Error("receipts show requires <receipt-id|latest>");
|
|
659
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
660
|
+
const receipt = receiptId === "latest" ? (await listReceiptsForInspection(targetStore))[0] : await targetStore.getReceiptForDisplayReadOnly(receiptId);
|
|
661
|
+
if (!receipt)
|
|
662
|
+
throw new Error(receiptId === "latest" ? "No receipts found" : `Receipt not found: ${receiptId}`);
|
|
663
|
+
io.stdout(JSON.stringify(receipt, null, 2));
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
666
|
+
throw unknownSubcommandError("receipts", subcommand, ["list", "show"]);
|
|
667
|
+
}
|
|
668
|
+
if (command === "sessions") {
|
|
669
|
+
const [subcommand, ...sessionArgs] = rest;
|
|
670
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
671
|
+
assertNoExtraArgs(sessionArgs, "sessions help", 0);
|
|
672
|
+
printSessionsHelp(io.stdout);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
if (subcommand === "list") {
|
|
676
|
+
if (printHelpIfRequested(sessionArgs, "sessions list", io.stdout, printSessionsHelp, { valueFlags: ["--cwd", "--status"] }))
|
|
677
|
+
return 0;
|
|
678
|
+
assertOnlyOptions(sessionArgs, "sessions list", ["--cwd", "--status"]);
|
|
679
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, sessionArgs));
|
|
680
|
+
const status = readSessionStatusFlag(sessionArgs);
|
|
681
|
+
const sessions = await listSessionsForInspection(targetStore, status);
|
|
682
|
+
for (const session of sessions) {
|
|
683
|
+
io.stdout(`${session.id}\t${session.status}\t${session.backend}\t${session.direction}`);
|
|
684
|
+
}
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
if (subcommand === "show") {
|
|
688
|
+
if (printHelpIfRequested(sessionArgs, "sessions show", io.stdout, printSessionsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
689
|
+
return 0;
|
|
690
|
+
const [sessionId] = readPositionalsWithOptions(sessionArgs, "sessions show", 1, ["--cwd"]);
|
|
691
|
+
if (!sessionId)
|
|
692
|
+
throw new Error("sessions show requires <session-id|latest>");
|
|
693
|
+
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, sessionArgs));
|
|
694
|
+
const session = sessionId === "latest" ? (await listSessionsForInspection(targetStore))[0] : await targetStore.getSessionReadOnly(sessionId);
|
|
695
|
+
if (!session)
|
|
696
|
+
throw new Error(sessionId === "latest" ? "No sessions found" : `Session not found: ${sessionId}`);
|
|
697
|
+
io.stdout(formatSession(session));
|
|
698
|
+
return 0;
|
|
699
|
+
}
|
|
700
|
+
throw unknownSubcommandError("sessions", subcommand, ["list", "show"]);
|
|
701
|
+
}
|
|
702
|
+
if (command === "pro") {
|
|
703
|
+
const [subcommand, ...proArgs] = rest;
|
|
704
|
+
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
705
|
+
assertNoExtraArgs(proArgs, "pro help", 0);
|
|
706
|
+
printProHelp(io.stdout);
|
|
707
|
+
return 0;
|
|
708
|
+
}
|
|
709
|
+
if (subcommand === "ask") {
|
|
710
|
+
if (printHelpIfRequested(proArgs, "pro ask", io.stdout, printProHelp, {
|
|
711
|
+
valueFlags: [...ASK_PRO_PREVIEW_VALUE_FLAGS],
|
|
712
|
+
booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
|
|
713
|
+
})) {
|
|
714
|
+
return 0;
|
|
715
|
+
}
|
|
716
|
+
parseAskProArgs(proArgs, ASK_PRO_PREVIEW_VALUE_FLAGS);
|
|
717
|
+
if (hasAskProSendMode(proArgs)) {
|
|
718
|
+
throw new Error("prodex pro ask is a dry-run preview. Use `prodex pro browser ask` for visible-browser sends.");
|
|
719
|
+
}
|
|
720
|
+
const hasDryRun = hasAskProDryRunMode(proArgs);
|
|
721
|
+
return runCli(["ask-pro", ...(hasDryRun ? [] : ["--dry-run"]), ...proArgs], io);
|
|
722
|
+
}
|
|
723
|
+
if (subcommand === "browser") {
|
|
724
|
+
const [browserSubcommand, ...browserArgs] = proArgs;
|
|
725
|
+
if (!browserSubcommand || isHelpSubcommand(browserSubcommand)) {
|
|
726
|
+
assertOnlyOptions(browserArgs, "pro browser help", ["--source-cli"]);
|
|
727
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
728
|
+
printProBrowserHelp(io.stdout, sourceCli);
|
|
729
|
+
return 0;
|
|
730
|
+
}
|
|
731
|
+
if (browserSubcommand === "login") {
|
|
732
|
+
if (printProBrowserHelpIfRequested(browserArgs, "pro browser login", io, {
|
|
733
|
+
valueFlags: ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"],
|
|
734
|
+
booleanFlags: ["--dry-run"]
|
|
735
|
+
})) {
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
assertOnlyOptions(browserArgs, "pro browser login", ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"], ["--dry-run"]);
|
|
739
|
+
const loginUrl = readChatGptBrowserUrlFlag(browserArgs);
|
|
740
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
741
|
+
const targetCwd = readFlag(browserArgs, "--cwd") ? resolveCwdFlag(io.cwd, browserArgs) : undefined;
|
|
742
|
+
const profileDir = readFlag(browserArgs, "--profile-dir");
|
|
743
|
+
const port = readPortFlag(browserArgs, "--port") ?? 9333;
|
|
744
|
+
const launchTimeoutMs = readPositiveNumberFlag(browserArgs, "--launch-timeout-ms");
|
|
745
|
+
const commandOptions = {
|
|
746
|
+
...(targetCwd ? { cwd: targetCwd } : {}),
|
|
747
|
+
...(profileDir ? { profileDir } : {}),
|
|
748
|
+
...(port !== 9333 ? { port } : {}),
|
|
749
|
+
...(readFlag(browserArgs, "--url") ? { url: loginUrl } : {}),
|
|
750
|
+
...(launchTimeoutMs !== undefined ? { launchTimeoutMs } : {})
|
|
751
|
+
};
|
|
752
|
+
if (browserArgs.includes("--dry-run")) {
|
|
753
|
+
printBrowserLoginGuide(io.stdout, {
|
|
754
|
+
opened: false,
|
|
755
|
+
loginUrl,
|
|
756
|
+
profileDir: profileDir ?? defaultChatGptProfileDir(),
|
|
757
|
+
port,
|
|
758
|
+
sourceCli,
|
|
759
|
+
commandOptions
|
|
760
|
+
});
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
const opened = openChatGptBrowser({
|
|
764
|
+
port,
|
|
765
|
+
profileDir,
|
|
766
|
+
url: loginUrl
|
|
767
|
+
});
|
|
768
|
+
await assertBrowserLaunchStayedAlive(opened, launchTimeoutMs);
|
|
769
|
+
printBrowserLoginGuide(io.stdout, {
|
|
770
|
+
opened: true,
|
|
771
|
+
loginUrl,
|
|
772
|
+
profileDir: opened.profileDir,
|
|
773
|
+
port: opened.port,
|
|
774
|
+
sourceCli,
|
|
775
|
+
commandOptions
|
|
776
|
+
});
|
|
777
|
+
return 0;
|
|
778
|
+
}
|
|
779
|
+
if (browserSubcommand === "ask") {
|
|
780
|
+
if (printProBrowserHelpIfRequested(browserArgs, "pro browser ask", io, {
|
|
781
|
+
valueFlags: [...ASK_PRO_VALUE_FLAGS],
|
|
782
|
+
booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
|
|
783
|
+
})) {
|
|
784
|
+
return 0;
|
|
785
|
+
}
|
|
786
|
+
if (hasAskProDryRunMode(browserArgs) && hasAskProSendMode(browserArgs)) {
|
|
787
|
+
throw new Error("ask-pro cannot combine --dry-run and --send");
|
|
788
|
+
}
|
|
789
|
+
if (hasAskProDryRunMode(browserArgs)) {
|
|
790
|
+
throw new Error("prodex pro browser ask is an explicit visible-browser send. Use `prodex pro ask` for dry-run previews.");
|
|
791
|
+
}
|
|
792
|
+
const hasMode = hasAskProMode(browserArgs);
|
|
793
|
+
return runCli(["ask-pro", ...(hasMode ? [] : ["--send"]), ...browserArgs], { ...io, allowAskProBrowserSend: true });
|
|
794
|
+
}
|
|
795
|
+
if (browserSubcommand === "open" || browserSubcommand === "status" || browserSubcommand === "doctor") {
|
|
796
|
+
const replacement = browserSubcommand === "open" ? "login" : "check";
|
|
797
|
+
throw new Error(`Use \`prodex pro browser ${replacement}\` for explicit browser automation.`);
|
|
798
|
+
}
|
|
799
|
+
if (browserSubcommand === "smoke") {
|
|
800
|
+
if (printProBrowserHelpIfRequested(browserArgs, "pro browser smoke", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
801
|
+
return 0;
|
|
802
|
+
return runCli(["chatgpt", browserSubcommand, ...browserArgs], io);
|
|
803
|
+
}
|
|
804
|
+
if (browserSubcommand === "check") {
|
|
805
|
+
if (printProBrowserHelpIfRequested(browserArgs, "pro browser check", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
806
|
+
return 0;
|
|
807
|
+
assertOnlyOptions(browserArgs, "pro browser check", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
|
|
808
|
+
const targetCwd = resolveCwdFlag(io.cwd, browserArgs);
|
|
809
|
+
readPortFlag(browserArgs, "--port");
|
|
810
|
+
readPositiveNumberFlag(browserArgs, "--timeout-ms");
|
|
811
|
+
const healthy = await printProductCheck(new BridgeStore(targetCwd), io, browserArgs, targetCwd);
|
|
812
|
+
return healthy ? 0 : 1;
|
|
813
|
+
}
|
|
814
|
+
throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check"]);
|
|
815
|
+
}
|
|
816
|
+
if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
|
|
817
|
+
throw new Error(`Use \`prodex pro browser ${subcommand === "doctor" ? "check" : subcommand}\` for explicit browser automation.`);
|
|
818
|
+
}
|
|
819
|
+
if (subcommand === "list") {
|
|
820
|
+
if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
821
|
+
return 0;
|
|
822
|
+
assertOnlyOptions(proArgs, "pro list", ["--cwd", "--source-cli"]);
|
|
823
|
+
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
824
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
825
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
826
|
+
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
827
|
+
const consults = await listConsultListEntries(targetStore);
|
|
828
|
+
for (const entry of consults) {
|
|
829
|
+
if (entry.kind === "untrusted") {
|
|
830
|
+
io.stdout(`${entry.task.id}\tuntrusted\t${sourceAwareResultMessage(errorMessage(entry.error), sourceCli, answerOptions)}`);
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
io.stdout(`${entry.consult.task.id}\t${entry.consult.result.status}\t${formatProListSummary(entry.consult, sourceCli, answerOptions)}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
if (subcommand === "latest") {
|
|
839
|
+
if (printHelpIfRequested(proArgs, "pro latest", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
840
|
+
return 0;
|
|
841
|
+
assertOnlyOptions(proArgs, "pro latest", ["--cwd", "--source-cli"]);
|
|
842
|
+
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
843
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
844
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
845
|
+
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
846
|
+
let consult;
|
|
847
|
+
try {
|
|
848
|
+
consult = await latestTrustedConsult(targetStore);
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
throw sourceAwareResultError(error, sourceCli, answerOptions);
|
|
852
|
+
}
|
|
853
|
+
if (!consult)
|
|
854
|
+
throw new Error("No GPT Pro answers found");
|
|
855
|
+
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
856
|
+
return 0;
|
|
857
|
+
}
|
|
858
|
+
if (subcommand === "show") {
|
|
859
|
+
if (printHelpIfRequested(proArgs, "pro show", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"], maxPositionals: 1 }))
|
|
860
|
+
return 0;
|
|
861
|
+
const [taskId] = readPositionalsWithOptions(proArgs, "pro show", 1, ["--cwd", "--source-cli"]);
|
|
862
|
+
if (!taskId)
|
|
863
|
+
throw new Error("pro show requires <task-id|latest>");
|
|
864
|
+
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
865
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
866
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
867
|
+
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
868
|
+
let consult;
|
|
869
|
+
try {
|
|
870
|
+
consult = taskId === "latest" ? await latestTrustedConsult(targetStore) : await getConsult(targetStore, taskId, { readOnly: true });
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
throw sourceAwareResultError(error, sourceCli, answerOptions);
|
|
874
|
+
}
|
|
875
|
+
if (!consult)
|
|
876
|
+
throw new Error(taskId === "latest" ? "No GPT Pro answers found" : `GPT Pro answer not found: ${taskId}`);
|
|
877
|
+
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
878
|
+
return 0;
|
|
879
|
+
}
|
|
880
|
+
throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "list", "latest", "show"]);
|
|
881
|
+
}
|
|
882
|
+
if (command === "consults") {
|
|
883
|
+
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
884
|
+
}
|
|
885
|
+
if (command === "ask-pro") {
|
|
886
|
+
const parsedAskPro = parseAskProArgs(rest);
|
|
887
|
+
const hasDryRunMode = parsedAskPro.optionArgs.includes("--dry-run");
|
|
888
|
+
const hasSendMode = parsedAskPro.optionArgs.includes("--send");
|
|
889
|
+
if (!hasDryRunMode && !hasSendMode) {
|
|
890
|
+
throw new Error("ask-pro requires --dry-run or --send");
|
|
891
|
+
}
|
|
892
|
+
if (hasDryRunMode && hasSendMode) {
|
|
893
|
+
throw new Error("ask-pro cannot combine --dry-run and --send");
|
|
894
|
+
}
|
|
895
|
+
if (hasSendMode && !io.allowAskProBrowserSend) {
|
|
896
|
+
throw new Error("Direct ask-pro --send is disabled. Use `prodex pro browser ask` for explicit visible-browser sends.");
|
|
897
|
+
}
|
|
898
|
+
const targetCwd = resolveCwdFlag(io.cwd, parsedAskPro.optionArgs);
|
|
899
|
+
const targetStore = new BridgeStore(targetCwd);
|
|
900
|
+
const files = readRepeatedFlag(parsedAskPro.optionArgs, "--file");
|
|
901
|
+
const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
|
|
902
|
+
const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
|
|
903
|
+
if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
904
|
+
throw new Error("--confirm-target requires --target-url so the visible browser target is explicit.");
|
|
905
|
+
}
|
|
906
|
+
if (normalizedTargetUrl && hasSendMode && !parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
907
|
+
throw new Error("--target-url requires --confirm-target after you manually verify the visible ChatGPT tab is the intended Project/thread.");
|
|
908
|
+
}
|
|
909
|
+
const prompt = parsedAskPro.promptParts.join(" ").trim();
|
|
910
|
+
if (!prompt)
|
|
911
|
+
throw new Error("ask-pro requires a prompt");
|
|
912
|
+
const browserPort = hasSendMode ? (readPortFlag(parsedAskPro.optionArgs, "--port") ?? 9333) : undefined;
|
|
913
|
+
const browserTimeoutMs = hasSendMode ? (readPositiveNumberFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? 90000) : undefined;
|
|
914
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, parsedAskPro.optionArgs, "--source-cli");
|
|
915
|
+
const bundle = await buildDryRunBundle(targetCwd, { prompt, files });
|
|
916
|
+
if (hasSendMode) {
|
|
917
|
+
const browserCommandOptions = {
|
|
918
|
+
cwd: targetCwd,
|
|
919
|
+
port: parsedAskPro.optionArgs.includes("--port") ? browserPort : undefined
|
|
920
|
+
};
|
|
921
|
+
const task = await targetStore.createTask({
|
|
922
|
+
source: "codex",
|
|
923
|
+
title: "GPT Pro consult",
|
|
924
|
+
prompt: bundle.text,
|
|
925
|
+
repo_id: "default",
|
|
926
|
+
files: files.map((file) => ({ path: file, role: "context" })),
|
|
927
|
+
provenance: {
|
|
928
|
+
adapter: "chatgpt-control",
|
|
929
|
+
session_id: bundle.id,
|
|
930
|
+
thread: normalizedTargetUrl,
|
|
931
|
+
warnings: []
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
await targetStore.claimTask(task.id, "chatgpt-pro");
|
|
935
|
+
try {
|
|
936
|
+
await writeSessionBeforeBrowserSend(targetStore, {
|
|
937
|
+
id: bundle.id,
|
|
938
|
+
direction: "codex_to_chatgpt",
|
|
939
|
+
backend: "chatgpt-control",
|
|
940
|
+
task_id: task.id,
|
|
941
|
+
thread: normalizedTargetUrl,
|
|
942
|
+
status: "running",
|
|
943
|
+
warnings: []
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
const blocker = {
|
|
948
|
+
code: "session_record_failed",
|
|
949
|
+
message: `Could not record ChatGPT browser session before send: ${errorMessage(error)}`,
|
|
950
|
+
retryable: true,
|
|
951
|
+
next_step: "Fix local .bridge write permissions, then rerun the consult."
|
|
952
|
+
};
|
|
953
|
+
try {
|
|
954
|
+
await targetStore.completeTask(task.id, {
|
|
955
|
+
status: "blocked",
|
|
956
|
+
summary: blocker.message,
|
|
957
|
+
commands: ["visible ChatGPT browser consult"],
|
|
958
|
+
blocker
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
catch (recordError) {
|
|
962
|
+
throw new Error(`${blocker.message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
|
|
963
|
+
}
|
|
964
|
+
throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
|
|
965
|
+
}
|
|
966
|
+
let consult;
|
|
967
|
+
try {
|
|
968
|
+
consult = await sendChatGptPrompt({
|
|
969
|
+
port: browserPort,
|
|
970
|
+
prompt: bundle.text,
|
|
971
|
+
targetUrl: normalizedTargetUrl,
|
|
972
|
+
timeoutMs: browserTimeoutMs
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, browserCommandOptions);
|
|
977
|
+
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
978
|
+
try {
|
|
979
|
+
await targetStore.completeTask(task.id, {
|
|
980
|
+
status: "blocked",
|
|
981
|
+
summary: message,
|
|
982
|
+
commands: ["visible ChatGPT browser consult"],
|
|
983
|
+
blocker
|
|
984
|
+
});
|
|
985
|
+
await writeSessionBestEffort(targetStore, {
|
|
986
|
+
id: bundle.id,
|
|
987
|
+
direction: "codex_to_chatgpt",
|
|
988
|
+
backend: "chatgpt-control",
|
|
989
|
+
task_id: task.id,
|
|
990
|
+
thread: normalizedTargetUrl,
|
|
991
|
+
status: "blocked",
|
|
992
|
+
blocker,
|
|
993
|
+
warnings: []
|
|
994
|
+
}, io);
|
|
995
|
+
}
|
|
996
|
+
catch (recordError) {
|
|
997
|
+
throw new Error(`${message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
|
|
998
|
+
}
|
|
999
|
+
throw new Error(formatBlockedConsultRecordedMessage(message, task.id, sourceCli, { cwd: targetCwd }));
|
|
1000
|
+
}
|
|
1001
|
+
const answerArtifactText = formatProConsultArtifact(consult);
|
|
1002
|
+
const persistenceWarnings = [...consult.warnings];
|
|
1003
|
+
let answerArtifactPath;
|
|
1004
|
+
const answerArtifactBytes = Buffer.byteLength(answerArtifactText, "utf8");
|
|
1005
|
+
if (answerArtifactBytes > MAX_FETCHABLE_RESULT_ARTIFACT_BYTES) {
|
|
1006
|
+
const warning = `answer_artifact_warning: answer artifact is too large for bridge_fetch_result_artifact (${answerArtifactBytes} bytes > ${MAX_FETCHABLE_RESULT_ARTIFACT_BYTES} bytes); saved answer in result summary only`;
|
|
1007
|
+
persistenceWarnings.push(warning);
|
|
1008
|
+
io.stderr(warning);
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
try {
|
|
1012
|
+
answerArtifactPath = await targetStore.writeArtifactText(`.bridge/artifacts/pro-consults/${task.id}.md`, answerArtifactText);
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
const warning = `answer_artifact_warning: ${errorMessage(error)}`;
|
|
1016
|
+
persistenceWarnings.push(warning);
|
|
1017
|
+
io.stderr(warning);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
await targetStore.writeReceipt({
|
|
1022
|
+
kind: "consult_answer_saved",
|
|
1023
|
+
task_id: task.id,
|
|
1024
|
+
session_id: bundle.id,
|
|
1025
|
+
summary: `Recorded ChatGPT answer for ${task.id}`,
|
|
1026
|
+
metadata: {
|
|
1027
|
+
...(answerArtifactPath ? { artifact_path: answerArtifactPath } : {}),
|
|
1028
|
+
thread: consult.url,
|
|
1029
|
+
warnings: persistenceWarnings
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
const warning = `receipt_record_warning: ${errorMessage(error)}`;
|
|
1035
|
+
persistenceWarnings.push(warning);
|
|
1036
|
+
io.stderr(warning);
|
|
1037
|
+
}
|
|
1038
|
+
let result;
|
|
1039
|
+
try {
|
|
1040
|
+
result = await targetStore.completeTask(task.id, {
|
|
1041
|
+
status: "done",
|
|
1042
|
+
summary: consult.answer,
|
|
1043
|
+
artifacts: answerArtifactPath ? [{ path: answerArtifactPath, role: "result", bytes: Buffer.byteLength(answerArtifactText, "utf8") }] : [],
|
|
1044
|
+
commands: ["visible ChatGPT browser consult"],
|
|
1045
|
+
warnings: persistenceWarnings,
|
|
1046
|
+
provenance: {
|
|
1047
|
+
thread: consult.url,
|
|
1048
|
+
warnings: persistenceWarnings
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
io.stdout(`consult_answer_received_but_not_saved: ${task.id} ${consult.url}`);
|
|
1054
|
+
io.stdout("");
|
|
1055
|
+
io.stdout(consult.answer);
|
|
1056
|
+
throw new Error(`ChatGPT answer was received but local persistence failed: ${errorMessage(error)}`);
|
|
1057
|
+
}
|
|
1058
|
+
await writeSessionBestEffort(targetStore, {
|
|
1059
|
+
id: bundle.id,
|
|
1060
|
+
direction: "codex_to_chatgpt",
|
|
1061
|
+
backend: "chatgpt-control",
|
|
1062
|
+
task_id: task.id,
|
|
1063
|
+
thread: consult.url,
|
|
1064
|
+
status: "done",
|
|
1065
|
+
warnings: persistenceWarnings
|
|
1066
|
+
}, io);
|
|
1067
|
+
io.stdout(`${result.task_id}\t${result.status}\t${consult.url}`);
|
|
1068
|
+
io.stdout("");
|
|
1069
|
+
io.stdout(result.summary);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
await writeSessionBestEffort(targetStore, {
|
|
1073
|
+
id: bundle.id,
|
|
1074
|
+
direction: "codex_to_chatgpt",
|
|
1075
|
+
backend: "manual",
|
|
1076
|
+
status: "preview",
|
|
1077
|
+
warnings: []
|
|
1078
|
+
}, io);
|
|
1079
|
+
await targetStore.writeReceipt({
|
|
1080
|
+
kind: "consult_preview",
|
|
1081
|
+
session_id: bundle.id,
|
|
1082
|
+
summary: `Created dry-run consult preview ${bundle.id}`
|
|
1083
|
+
});
|
|
1084
|
+
io.stdout(`DRY RUN ${bundle.id}`);
|
|
1085
|
+
io.stdout(bundle.text);
|
|
1086
|
+
}
|
|
1087
|
+
return 0;
|
|
1088
|
+
}
|
|
1089
|
+
if (command === "mcp") {
|
|
1090
|
+
if (printHelpIfRequested(rest, "mcp", io.stdout, printMcpHelp, { valueFlags: ["--cwd"] }))
|
|
1091
|
+
return 0;
|
|
1092
|
+
assertOnlyOptions(rest, "mcp", ["--cwd"]);
|
|
1093
|
+
await runMcpServer(resolveCwdFlag(io.cwd, rest));
|
|
1094
|
+
return 0;
|
|
1095
|
+
}
|
|
1096
|
+
throw unknownTopLevelCommandError(command);
|
|
1097
|
+
}
|
|
1098
|
+
function defaultIo() {
|
|
1099
|
+
return {
|
|
1100
|
+
cwd: process.cwd(),
|
|
1101
|
+
stdout: (line) => console.log(line),
|
|
1102
|
+
stderr: (line) => console.error(line)
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function printHelp(stdout) {
|
|
1106
|
+
stdout(`prodex v${CLI_VERSION}
|
|
1107
|
+
|
|
1108
|
+
Commands:
|
|
1109
|
+
prodex --version
|
|
1110
|
+
prodex init [--cwd /absolute/path/to/repo]
|
|
1111
|
+
prodex doctor [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1112
|
+
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>]
|
|
1113
|
+
prodex start [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1114
|
+
prodex status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] [--show-token] [--url-only] [--unsafe-show-non-expiring-token]
|
|
1115
|
+
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
1116
|
+
prodex release status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1117
|
+
prodex release pack [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --pack-destination /absolute/path [--keep-workdir]
|
|
1118
|
+
prodex onboard [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1119
|
+
prodex project prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1120
|
+
prodex claude prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1121
|
+
prodex claude config [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1122
|
+
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] "prompt" # dry-run preview
|
|
1123
|
+
prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000] # preview/open visible browser login
|
|
1124
|
+
prodex pro browser help [--source-cli /absolute/path/to/dist/cli.js]
|
|
1125
|
+
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]
|
|
1126
|
+
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]
|
|
1127
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--file path] "prompt" # explicit visible-browser send
|
|
1128
|
+
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1129
|
+
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1130
|
+
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1131
|
+
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt"
|
|
1132
|
+
prodex tasks list [--status new|claimed|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1133
|
+
prodex tasks show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1134
|
+
prodex tasks claim <task-id> [--cwd /absolute/path/to/repo] [--by codex]
|
|
1135
|
+
prodex tasks complete <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--command "npm test"] [--artifact .bridge/artifacts/results/name.md=text]
|
|
1136
|
+
prodex tasks block <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--code code] [--next-step "Next step"] [--retryable]
|
|
1137
|
+
prodex results show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1138
|
+
prodex results artifact <task-id|latest> [artifact-path] [--cwd /absolute/path/to/repo]
|
|
1139
|
+
prodex results reseal <task-id|latest> --confirm-current-result [--cwd /absolute/path/to/repo]
|
|
1140
|
+
prodex receipts list [--kind kind] [--task-id task-id] [--cwd /absolute/path/to/repo]
|
|
1141
|
+
prodex receipts show <receipt-id|latest> [--cwd /absolute/path/to/repo]
|
|
1142
|
+
prodex sessions list [--status preview|running|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1143
|
+
prodex sessions show <session-id|latest> [--cwd /absolute/path/to/repo]
|
|
1144
|
+
prodex mcp [--cwd /absolute/path/to/repo]`);
|
|
1145
|
+
}
|
|
1146
|
+
function printInitHelp(stdout) {
|
|
1147
|
+
stdout(`prodex init
|
|
1148
|
+
|
|
1149
|
+
Commands:
|
|
1150
|
+
prodex init [--cwd /absolute/path/to/repo]
|
|
1151
|
+
|
|
1152
|
+
Initialize the local .bridge receipt ledger and bridge .gitignore entries.`);
|
|
1153
|
+
}
|
|
1154
|
+
function printSetupHelp(stdout) {
|
|
1155
|
+
stdout(`prodex setup
|
|
1156
|
+
|
|
1157
|
+
Commands:
|
|
1158
|
+
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>]
|
|
1159
|
+
|
|
1160
|
+
Save a loopback-only HTTP MCP profile in .bridge/config.local.json. Use --token-ttl-hours before tunnels or ChatGPT Project use.`);
|
|
1161
|
+
}
|
|
1162
|
+
function printStartHelp(stdout) {
|
|
1163
|
+
stdout(`prodex start
|
|
1164
|
+
|
|
1165
|
+
Commands:
|
|
1166
|
+
prodex start [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1167
|
+
|
|
1168
|
+
Start the local loopback HTTP MCP server from the saved setup profile.`);
|
|
1169
|
+
}
|
|
1170
|
+
function printStatusHelp(stdout) {
|
|
1171
|
+
stdout(`prodex status
|
|
1172
|
+
|
|
1173
|
+
Commands:
|
|
1174
|
+
prodex status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] [--show-token] [--url-only] [--unsafe-show-non-expiring-token]
|
|
1175
|
+
|
|
1176
|
+
Show the saved local MCP URL with tokens redacted by default.`);
|
|
1177
|
+
}
|
|
1178
|
+
function printTunnelHelp(stdout) {
|
|
1179
|
+
stdout(`prodex tunnel
|
|
1180
|
+
|
|
1181
|
+
Commands:
|
|
1182
|
+
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
1183
|
+
|
|
1184
|
+
Format a public tunnel MCP URL from an existing local setup. This command does not create a tunnel.`);
|
|
1185
|
+
}
|
|
1186
|
+
function printTunnelUrlHelp(stdout) {
|
|
1187
|
+
stdout(`prodex tunnel url
|
|
1188
|
+
|
|
1189
|
+
Commands:
|
|
1190
|
+
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
1191
|
+
|
|
1192
|
+
This command does not create a tunnel. It only formats your supplied public URL with the saved short-lived MCP token.`);
|
|
1193
|
+
}
|
|
1194
|
+
function printDoctorHelp(stdout) {
|
|
1195
|
+
stdout(`prodex doctor
|
|
1196
|
+
|
|
1197
|
+
Commands:
|
|
1198
|
+
prodex doctor [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1199
|
+
|
|
1200
|
+
Run local bridge, MCP, write/apply/stage, and HTTP MCP smoke checks without opening ChatGPT.`);
|
|
1201
|
+
}
|
|
1202
|
+
function printOnboardHelp(stdout) {
|
|
1203
|
+
stdout(`prodex onboard
|
|
1204
|
+
|
|
1205
|
+
Commands:
|
|
1206
|
+
prodex onboard [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1207
|
+
|
|
1208
|
+
Print a local-first setup guide for Codex, ChatGPT Projects, Claude, and visible-browser Pro consults.`);
|
|
1209
|
+
}
|
|
1210
|
+
function printMcpHelp(stdout) {
|
|
1211
|
+
stdout(`prodex mcp
|
|
1212
|
+
|
|
1213
|
+
Commands:
|
|
1214
|
+
prodex mcp [--cwd /absolute/path/to/repo]
|
|
1215
|
+
|
|
1216
|
+
Run the stdio MCP server for local clients such as Claude. This does not reveal HTTP MCP URL tokens.`);
|
|
1217
|
+
}
|
|
1218
|
+
function printReleaseHelp(stdout) {
|
|
1219
|
+
stdout(`prodex release
|
|
1220
|
+
|
|
1221
|
+
Commands:
|
|
1222
|
+
prodex release status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1223
|
+
prodex release pack [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --pack-destination /absolute/path [--keep-workdir]
|
|
1224
|
+
|
|
1225
|
+
Release commands are local checks and package preparation helpers; they do not publish or push.`);
|
|
1226
|
+
}
|
|
1227
|
+
function printProHelp(stdout) {
|
|
1228
|
+
stdout(`prodex pro
|
|
1229
|
+
|
|
1230
|
+
Commands:
|
|
1231
|
+
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] "prompt"
|
|
1232
|
+
prodex pro browser help [--source-cli /absolute/path/to/dist/cli.js]
|
|
1233
|
+
prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--launch-timeout-ms 5000]
|
|
1234
|
+
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1235
|
+
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1236
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--file path] "prompt"
|
|
1237
|
+
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1238
|
+
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1239
|
+
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1240
|
+
|
|
1241
|
+
Use \`prodex pro ask\` for dry-run/manual previews.
|
|
1242
|
+
Use \`prodex pro browser ask\` only when you want an explicit visible-browser send.`);
|
|
1243
|
+
}
|
|
1244
|
+
function printProjectHelp(stdout) {
|
|
1245
|
+
stdout(`prodex project
|
|
1246
|
+
|
|
1247
|
+
Commands:
|
|
1248
|
+
prodex project prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1249
|
+
|
|
1250
|
+
Print a ChatGPT Project MCP verification prompt. The prompt asks for read/task handoff verification only.`);
|
|
1251
|
+
}
|
|
1252
|
+
function printClaudeHelp(stdout) {
|
|
1253
|
+
stdout(`prodex claude
|
|
1254
|
+
|
|
1255
|
+
Commands:
|
|
1256
|
+
prodex claude prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1257
|
+
prodex claude config [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1258
|
+
|
|
1259
|
+
Print Claude MCP setup and verification helpers. These commands do not start MCP or reveal HTTP tokens.`);
|
|
1260
|
+
}
|
|
1261
|
+
function printTasksHelp(stdout) {
|
|
1262
|
+
stdout(`prodex tasks
|
|
1263
|
+
|
|
1264
|
+
Commands:
|
|
1265
|
+
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt"
|
|
1266
|
+
prodex tasks list [--status new|claimed|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1267
|
+
prodex tasks show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1268
|
+
prodex tasks claim <task-id> [--cwd /absolute/path/to/repo] [--by codex]
|
|
1269
|
+
prodex tasks complete <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--command "npm test"] [--artifact .bridge/artifacts/results/name.md=text]
|
|
1270
|
+
prodex tasks block <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--code code] [--next-step "Next step"] [--retryable]`);
|
|
1271
|
+
}
|
|
1272
|
+
function printResultsHelp(stdout) {
|
|
1273
|
+
stdout(`prodex results
|
|
1274
|
+
|
|
1275
|
+
Commands:
|
|
1276
|
+
prodex results show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1277
|
+
prodex results artifact <task-id|latest> [artifact-path] [--cwd /absolute/path/to/repo]
|
|
1278
|
+
prodex results reseal <task-id|latest> --confirm-current-result [--cwd /absolute/path/to/repo]`);
|
|
1279
|
+
}
|
|
1280
|
+
function printReceiptsHelp(stdout) {
|
|
1281
|
+
stdout(`prodex receipts
|
|
1282
|
+
|
|
1283
|
+
Commands:
|
|
1284
|
+
prodex receipts list [--kind kind] [--task-id task-id] [--cwd /absolute/path/to/repo]
|
|
1285
|
+
prodex receipts show <receipt-id|latest> [--cwd /absolute/path/to/repo]`);
|
|
1286
|
+
}
|
|
1287
|
+
function printSessionsHelp(stdout) {
|
|
1288
|
+
stdout(`prodex sessions
|
|
1289
|
+
|
|
1290
|
+
Commands:
|
|
1291
|
+
prodex sessions list [--status preview|running|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1292
|
+
prodex sessions show <session-id|latest> [--cwd /absolute/path/to/repo]`);
|
|
1293
|
+
}
|
|
1294
|
+
function isHelpSubcommand(value) {
|
|
1295
|
+
return value === "help" || value === "--help" || value === "-h";
|
|
1296
|
+
}
|
|
1297
|
+
function isHelpArgs(args) {
|
|
1298
|
+
return args.length > 0 && isHelpSubcommand(args[0]);
|
|
1299
|
+
}
|
|
1300
|
+
function printHelpIfRequested(args, command, stdout, printHelp, options = {}) {
|
|
1301
|
+
const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
|
|
1302
|
+
if (helpIndex === -1)
|
|
1303
|
+
return false;
|
|
1304
|
+
assertHelpRequestArgs(args, command, options);
|
|
1305
|
+
printHelp(stdout);
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
function printProBrowserHelpIfRequested(args, command, io, options) {
|
|
1309
|
+
const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
|
|
1310
|
+
if (helpIndex === -1)
|
|
1311
|
+
return false;
|
|
1312
|
+
assertHelpRequestArgs(args, command, options);
|
|
1313
|
+
printProBrowserHelp(io.stdout, resolveOptionalFileFlag(io.cwd, args, "--source-cli"));
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
function findHelpFlagIndexBeforePromptDelimiter(args) {
|
|
1317
|
+
const delimiterIndex = args.indexOf("--");
|
|
1318
|
+
const limit = delimiterIndex === -1 ? args.length : delimiterIndex;
|
|
1319
|
+
return args.findIndex((arg, index) => index < limit && isHelpSubcommand(arg));
|
|
1320
|
+
}
|
|
1321
|
+
function assertHelpRequestArgs(args, command, options) {
|
|
1322
|
+
const delimiterIndex = args.indexOf("--");
|
|
1323
|
+
const commandArgs = delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
1324
|
+
const valueFlagSet = new Set(options.valueFlags ?? []);
|
|
1325
|
+
const booleanFlagSet = new Set(options.booleanFlags ?? []);
|
|
1326
|
+
const maxPositionals = options.maxPositionals ?? 0;
|
|
1327
|
+
let positionals = 0;
|
|
1328
|
+
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
1329
|
+
const arg = commandArgs[index];
|
|
1330
|
+
if (isHelpSubcommand(arg))
|
|
1331
|
+
continue;
|
|
1332
|
+
if (valueFlagSet.has(arg)) {
|
|
1333
|
+
const next = commandArgs[index + 1];
|
|
1334
|
+
if (next && !isHelpSubcommand(next)) {
|
|
1335
|
+
readFlagValue(commandArgs, index, arg);
|
|
1336
|
+
index += 1;
|
|
1337
|
+
}
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (booleanFlagSet.has(arg))
|
|
1341
|
+
continue;
|
|
1342
|
+
if (arg.startsWith("-")) {
|
|
1343
|
+
throw unknownOptionError(arg, command, [...valueFlagSet, ...booleanFlagSet]);
|
|
1344
|
+
}
|
|
1345
|
+
if (positionals >= maxPositionals) {
|
|
1346
|
+
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
1347
|
+
}
|
|
1348
|
+
positionals += 1;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
function unknownSubcommandError(command, subcommand, expected) {
|
|
1352
|
+
const suggestion = closestSuggestion(subcommand, expected);
|
|
1353
|
+
const suggestionText = suggestion ? ` Did you mean \`prodex ${command} ${suggestion}\`?` : "";
|
|
1354
|
+
return new Error(`Unknown ${command} subcommand: ${subcommand}.${suggestionText} Expected one of: ${expected.join(", ")}. Run \`prodex ${command} --help\`.`);
|
|
1355
|
+
}
|
|
1356
|
+
function unknownTopLevelCommandError(command) {
|
|
1357
|
+
const suggestion = closestSuggestion(command, TOP_LEVEL_COMMANDS);
|
|
1358
|
+
const suggestionText = suggestion ? ` Did you mean \`prodex ${suggestion}\`?` : "";
|
|
1359
|
+
return new Error(`Unknown command: ${command}.${suggestionText} Run \`prodex help\`.`);
|
|
1360
|
+
}
|
|
1361
|
+
function unknownOptionError(option, command, candidates) {
|
|
1362
|
+
const suggestion = closestSuggestion(option, candidates);
|
|
1363
|
+
const suggestionText = suggestion ? `. Did you mean \`${suggestion}\`?` : "";
|
|
1364
|
+
const context = command ? ` for ${command}` : "";
|
|
1365
|
+
return new Error(`Unknown option${context}: ${option}${suggestionText}`);
|
|
1366
|
+
}
|
|
1367
|
+
function closestSuggestion(value, candidates) {
|
|
1368
|
+
let best;
|
|
1369
|
+
for (const candidate of candidates) {
|
|
1370
|
+
const distance = editDistance(value, candidate);
|
|
1371
|
+
const prefixMatch = isUsefulPrefixSuggestion(value, candidate);
|
|
1372
|
+
if (!best || (prefixMatch && !best.prefixMatch) || (prefixMatch === best.prefixMatch && distance < best.distance)) {
|
|
1373
|
+
best = { command: candidate, distance, prefixMatch };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return best && (best.prefixMatch || best.distance <= 2) ? best.command : undefined;
|
|
1377
|
+
}
|
|
1378
|
+
function isUsefulPrefixSuggestion(value, candidate) {
|
|
1379
|
+
return value.length >= 5 && candidate.startsWith(value);
|
|
1380
|
+
}
|
|
1381
|
+
function editDistance(left, right) {
|
|
1382
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
1383
|
+
const current = Array.from({ length: right.length + 1 }, () => 0);
|
|
1384
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
1385
|
+
current[0] = leftIndex;
|
|
1386
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
1387
|
+
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
1388
|
+
current[rightIndex] = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + substitutionCost);
|
|
1389
|
+
}
|
|
1390
|
+
previous.splice(0, previous.length, ...current);
|
|
1391
|
+
}
|
|
1392
|
+
return previous[right.length];
|
|
1393
|
+
}
|
|
1394
|
+
function legacyChatGptNamespaceError(subcommand) {
|
|
1395
|
+
const prefix = subcommand ? `Unknown legacy chatgpt subcommand: ${subcommand}.` : "The legacy `chatgpt` namespace is hidden.";
|
|
1396
|
+
return new Error(`${prefix} Use \`prodex pro browser help\` for visible-browser commands.`);
|
|
1397
|
+
}
|
|
1398
|
+
function formatProjectVerificationPrompt(cwd, sourceCli) {
|
|
1399
|
+
const cli = formatCliCommand(sourceCli);
|
|
1400
|
+
const quotedCwd = shellQuote(cwd);
|
|
1401
|
+
const sourceCliOption = formatSourceCliOption(sourceCli);
|
|
1402
|
+
return `ChatGPT Project MCP verification prompt
|
|
1403
|
+
|
|
1404
|
+
Paste this into the ChatGPT Project after adding the prodex MCP server URL.
|
|
1405
|
+
${TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING}
|
|
1406
|
+
|
|
1407
|
+
Please verify the prodex MCP bridge for this private project:
|
|
1408
|
+
|
|
1409
|
+
1. Call the MCP tool \`bridge_create_task\` with:
|
|
1410
|
+
|
|
1411
|
+
{
|
|
1412
|
+
"title": "prodex MCP verification",
|
|
1413
|
+
"prompt": "Verify that this ChatGPT Project can create tasks through the local prodex MCP bridge.",
|
|
1414
|
+
"repo_id": "default"
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
2. Call \`bridge_list_tasks\` with:
|
|
1418
|
+
|
|
1419
|
+
{ "status": "new" }
|
|
1420
|
+
|
|
1421
|
+
3. Call \`bridge_get_task\` with the task_id returned by \`bridge_create_task\`.
|
|
1422
|
+
|
|
1423
|
+
4. Reply with the task_id and whether all three MCP calls succeeded. Ask me to run the local completion command below, then wait.
|
|
1424
|
+
|
|
1425
|
+
5. After I reply exactly \`local completion done\`, call \`bridge_fetch_result\` with:
|
|
1426
|
+
|
|
1427
|
+
{ "task_id": "<task-id>" }
|
|
1428
|
+
|
|
1429
|
+
6. If the fetched result lists artifacts, call \`bridge_fetch_result_artifact\` for each listed result artifact path:
|
|
1430
|
+
|
|
1431
|
+
{ "task_id": "<task-id>", "path": "<artifact-path>" }
|
|
1432
|
+
|
|
1433
|
+
7. Reply with whether \`bridge_fetch_result\` returned the verification result summary and whether every listed result artifact was readable. Do not call repo_write_file_dry_run, repo_write_file_apply, repo_stage_reviewed_paths, or any write/stage tool for this verification.
|
|
1434
|
+
|
|
1435
|
+
Local follow-up after ChatGPT replies:
|
|
1436
|
+
|
|
1437
|
+
cd ${quotedCwd}
|
|
1438
|
+
${cli} tasks list --status new --cwd ${quotedCwd}
|
|
1439
|
+
${cli} tasks show <task-id> --cwd ${quotedCwd}
|
|
1440
|
+
${cli} tasks complete <task-id> --cwd ${quotedCwd} --summary "prodex MCP verification result" --artifact .bridge/artifacts/results/mcp-verification.md="prodex MCP verification artifact"
|
|
1441
|
+
|
|
1442
|
+
Then reply to ChatGPT with:
|
|
1443
|
+
|
|
1444
|
+
local completion done
|
|
1445
|
+
|
|
1446
|
+
If ChatGPT cannot see or call the MCP tools, keep the server terminal running and check locally:
|
|
1447
|
+
|
|
1448
|
+
${cli} status --cwd ${quotedCwd}${sourceCliOption}
|
|
1449
|
+
${cli} doctor --cwd ${quotedCwd}${sourceCliOption}`;
|
|
1450
|
+
}
|
|
1451
|
+
function formatOnboardingGuide(cwd, hasReadme, sourceCli) {
|
|
1452
|
+
const quotedCwd = shellQuote(cwd);
|
|
1453
|
+
const cli = sourceCli ? `node ${shellQuote(sourceCli)}` : "prodex";
|
|
1454
|
+
const sourceCliOption = sourceCli ? ` --source-cli ${shellQuote(sourceCli)}` : "";
|
|
1455
|
+
const proAskCommand = hasReadme ? `${cli} pro ask --cwd ${quotedCwd} --file README.md "Review this repo"` : `${cli} pro ask --cwd ${quotedCwd} "Review this repo"`;
|
|
1456
|
+
const proBrowserAskCommand = hasReadme
|
|
1457
|
+
? `${cli} pro browser ask${sourceCliOption} --cwd ${quotedCwd} --file README.md "Review this repo"`
|
|
1458
|
+
: `${cli} pro browser ask${sourceCliOption} --cwd ${quotedCwd} "Review this repo"`;
|
|
1459
|
+
return `prodex onboarding
|
|
1460
|
+
|
|
1461
|
+
repo: ${cwd}
|
|
1462
|
+
|
|
1463
|
+
1. Prepare the local bridge:
|
|
1464
|
+
${cli} init --cwd ${quotedCwd}
|
|
1465
|
+
${cli} doctor --cwd ${quotedCwd}${sourceCliOption}
|
|
1466
|
+
|
|
1467
|
+
2. Claude stdio MCP:
|
|
1468
|
+
${cli} claude config --cwd ${quotedCwd}${sourceCliOption}
|
|
1469
|
+
${cli} claude prompt --cwd ${quotedCwd}${sourceCliOption}
|
|
1470
|
+
|
|
1471
|
+
3. ChatGPT Project HTTP MCP:
|
|
1472
|
+
Note: HTTP MCP uses a short-lived token. Paste token-bearing URLs only into your own trusted private MCP client.
|
|
1473
|
+
${TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING}
|
|
1474
|
+
${cli} setup --cwd ${quotedCwd} --token-ttl-hours 24
|
|
1475
|
+
${cli} start --cwd ${quotedCwd}${sourceCliOption}
|
|
1476
|
+
Keep this terminal open while ChatGPT uses the bridge; run the next commands in a second terminal.
|
|
1477
|
+
${cli} status --cwd ${quotedCwd} --show-token --url-only${sourceCliOption}
|
|
1478
|
+
${cli} project prompt --cwd ${quotedCwd}${sourceCliOption}
|
|
1479
|
+
|
|
1480
|
+
4. Optional ChatGPT Pro consults:
|
|
1481
|
+
cd ${quotedCwd}
|
|
1482
|
+
${proAskCommand} # dry-run/manual preview
|
|
1483
|
+
${cli} pro browser login --dry-run${sourceCliOption} # preview, no browser opens
|
|
1484
|
+
${cli} pro browser login${sourceCliOption} # opens visible browser
|
|
1485
|
+
${cli} pro browser help${sourceCliOption}
|
|
1486
|
+
${cli} pro browser check${sourceCliOption} --cwd ${quotedCwd}
|
|
1487
|
+
${cli} pro browser smoke${sourceCliOption} --cwd ${quotedCwd}
|
|
1488
|
+
${proBrowserAskCommand} # visible-browser send
|
|
1489
|
+
${cli} pro list${sourceCliOption} --cwd ${quotedCwd}
|
|
1490
|
+
${cli} pro latest${sourceCliOption} --cwd ${quotedCwd}
|
|
1491
|
+
${cli} results show latest --cwd ${quotedCwd}
|
|
1492
|
+
${cli} results artifact latest --cwd ${quotedCwd}
|
|
1493
|
+
${cli} results reseal <task-id> --confirm-current-result --cwd ${quotedCwd} # only after reviewing .bridge/results/<task-id>.json
|
|
1494
|
+
|
|
1495
|
+
Safety notes:
|
|
1496
|
+
- This command only prints commands; it does not start servers, open browsers, or write files.
|
|
1497
|
+
- Visible-browser sends require a manual, visible browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers.`;
|
|
1498
|
+
}
|
|
1499
|
+
async function hasOnboardingReadme(cwd) {
|
|
1500
|
+
try {
|
|
1501
|
+
const stat = await lstat(path.join(cwd, "README.md"));
|
|
1502
|
+
return stat.isFile();
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
if (isMissingFileError(error))
|
|
1506
|
+
return false;
|
|
1507
|
+
throw error;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function formatClaudeVerificationPrompt(cwd, sourceCli) {
|
|
1511
|
+
const cli = formatCliCommand(sourceCli);
|
|
1512
|
+
const quotedCwd = shellQuote(cwd);
|
|
1513
|
+
const sourceCliOption = sourceCli ? ` --source-cli ${shellQuote(sourceCli)}` : "";
|
|
1514
|
+
return `Claude MCP verification prompt
|
|
1515
|
+
|
|
1516
|
+
Paste this into Claude after adding the prodex stdio MCP server.
|
|
1517
|
+
|
|
1518
|
+
Please verify the prodex MCP bridge for this private repo:
|
|
1519
|
+
|
|
1520
|
+
1. Call the MCP tool \`bridge_create_task\` with:
|
|
1521
|
+
|
|
1522
|
+
{
|
|
1523
|
+
"title": "prodex Claude MCP verification",
|
|
1524
|
+
"prompt": "Verify that Claude can create tasks through the local prodex MCP bridge.",
|
|
1525
|
+
"repo_id": "default"
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
2. Call \`bridge_list_tasks\` with:
|
|
1529
|
+
|
|
1530
|
+
{ "status": "new" }
|
|
1531
|
+
|
|
1532
|
+
3. Call \`bridge_get_task\` with the task_id returned by \`bridge_create_task\`.
|
|
1533
|
+
|
|
1534
|
+
4. Reply with the task_id and whether all three MCP calls succeeded. Ask me to run the local completion command below, then wait.
|
|
1535
|
+
|
|
1536
|
+
5. After I reply exactly \`local completion done\`, call \`bridge_fetch_result\` with:
|
|
1537
|
+
|
|
1538
|
+
{ "task_id": "<task-id>" }
|
|
1539
|
+
|
|
1540
|
+
6. If the fetched result lists artifacts, call \`bridge_fetch_result_artifact\` for each listed result artifact path:
|
|
1541
|
+
|
|
1542
|
+
{ "task_id": "<task-id>", "path": "<artifact-path>" }
|
|
1543
|
+
|
|
1544
|
+
7. Reply with whether \`bridge_fetch_result\` returned the verification result summary and whether every listed result artifact was readable. Do not call repo_write_file_dry_run, repo_write_file_apply, repo_stage_reviewed_paths, or any write/stage tool for this verification.
|
|
1545
|
+
|
|
1546
|
+
Local follow-up after Claude replies:
|
|
1547
|
+
|
|
1548
|
+
cd ${quotedCwd}
|
|
1549
|
+
${cli} tasks list --status new --cwd ${quotedCwd}
|
|
1550
|
+
${cli} tasks show <task-id> --cwd ${quotedCwd}
|
|
1551
|
+
${cli} tasks complete <task-id> --cwd ${quotedCwd} --summary "prodex Claude MCP verification result" --artifact .bridge/artifacts/results/claude-verification.md="prodex Claude MCP verification artifact"
|
|
1552
|
+
|
|
1553
|
+
Then reply to Claude with:
|
|
1554
|
+
|
|
1555
|
+
local completion done
|
|
1556
|
+
|
|
1557
|
+
If Claude cannot see or call the MCP tools, regenerate the config and run the local health check:
|
|
1558
|
+
|
|
1559
|
+
${cli} claude config --cwd ${quotedCwd}${sourceCliOption}
|
|
1560
|
+
${cli} doctor --cwd ${quotedCwd}${sourceCliOption}`;
|
|
1561
|
+
}
|
|
1562
|
+
function formatClaudeConfig(cwd, sourceCli) {
|
|
1563
|
+
return JSON.stringify({
|
|
1564
|
+
mcpServers: {
|
|
1565
|
+
prodex: sourceCli
|
|
1566
|
+
? { command: "node", args: [sourceCli, "mcp", "--cwd", cwd] }
|
|
1567
|
+
: { command: "prodex", args: ["mcp", "--cwd", cwd] }
|
|
1568
|
+
}
|
|
1569
|
+
}, null, 2);
|
|
1570
|
+
}
|
|
1571
|
+
function shellQuote(value) {
|
|
1572
|
+
return /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
1573
|
+
}
|
|
1574
|
+
function formatCliCommand(sourceCli) {
|
|
1575
|
+
return sourceCli ? `node ${shellQuote(sourceCli)}` : "prodex";
|
|
1576
|
+
}
|
|
1577
|
+
function formatInitCommand(sourceCli, options = {}) {
|
|
1578
|
+
return [`${formatCliCommand(sourceCli)} init`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined].filter(Boolean).join(" ");
|
|
1579
|
+
}
|
|
1580
|
+
function formatSetupCommand(sourceCli, options = {}) {
|
|
1581
|
+
return [`${formatCliCommand(sourceCli)} setup`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined].filter(Boolean).join(" ");
|
|
1582
|
+
}
|
|
1583
|
+
function formatSourceCliOption(sourceCli) {
|
|
1584
|
+
return sourceCli ? ` --source-cli ${shellQuote(sourceCli)}` : "";
|
|
1585
|
+
}
|
|
1586
|
+
function formatBrowserLoginCommand(sourceCli, options = {}) {
|
|
1587
|
+
return formatCommandInCwd(formatBrowserLoginCommandBody(sourceCli, options), options.cwd);
|
|
1588
|
+
}
|
|
1589
|
+
function formatBrowserLoginCommandBody(sourceCli, options = {}) {
|
|
1590
|
+
const command = [
|
|
1591
|
+
`${formatCliCommand(sourceCli)} pro browser login${formatSourceCliOption(sourceCli)}`,
|
|
1592
|
+
options.profileDir ? `--profile-dir ${shellQuote(options.profileDir)}` : undefined,
|
|
1593
|
+
options.port ? `--port ${options.port}` : undefined,
|
|
1594
|
+
options.url ? `--url ${shellQuote(options.url)}` : undefined,
|
|
1595
|
+
options.launchTimeoutMs ? `--launch-timeout-ms ${options.launchTimeoutMs}` : undefined
|
|
1596
|
+
]
|
|
1597
|
+
.filter(Boolean)
|
|
1598
|
+
.join(" ");
|
|
1599
|
+
return command;
|
|
1600
|
+
}
|
|
1601
|
+
function formatBrowserSmokeCommand(sourceCli, options = {}) {
|
|
1602
|
+
return formatCommandInCwd(formatBrowserSmokeCommandBody(sourceCli, options), options.cwd);
|
|
1603
|
+
}
|
|
1604
|
+
function formatBrowserSmokeCommandBody(sourceCli, options = {}) {
|
|
1605
|
+
const command = [`${formatCliCommand(sourceCli)} pro browser smoke${formatSourceCliOption(sourceCli)}`, options.port ? `--port ${options.port}` : undefined]
|
|
1606
|
+
.filter(Boolean)
|
|
1607
|
+
.join(" ");
|
|
1608
|
+
return command;
|
|
1609
|
+
}
|
|
1610
|
+
// ask retry commands carry their own --target-url/--confirm-target/"prompt" tail, so callers
|
|
1611
|
+
// preserve the stored argument tail verbatim and only re-point this bare base at the source CLI.
|
|
1612
|
+
function formatBrowserAskCommandBody(sourceCli) {
|
|
1613
|
+
return `${formatCliCommand(sourceCli)} pro browser ask${formatSourceCliOption(sourceCli)}`;
|
|
1614
|
+
}
|
|
1615
|
+
function formatBrowserCheckCommand(sourceCli, options = {}) {
|
|
1616
|
+
const command = [`${formatCliCommand(sourceCli)} pro browser check${formatSourceCliOption(sourceCli)}`, options.port ? `--port ${options.port}` : undefined]
|
|
1617
|
+
.filter(Boolean)
|
|
1618
|
+
.join(" ");
|
|
1619
|
+
return formatCommandInCwd(command, options.cwd);
|
|
1620
|
+
}
|
|
1621
|
+
function formatBrowserTargetAskCommand(sourceCli, options = {}) {
|
|
1622
|
+
const command = [
|
|
1623
|
+
`${formatCliCommand(sourceCli)} pro browser ask${formatSourceCliOption(sourceCli)}`,
|
|
1624
|
+
options.port ? `--port ${options.port}` : undefined,
|
|
1625
|
+
`--target-url ${options.targetUrl ? shellQuote(options.targetUrl) : "<chatgpt-url>"} --confirm-target "prompt"`
|
|
1626
|
+
]
|
|
1627
|
+
.filter(Boolean)
|
|
1628
|
+
.join(" ");
|
|
1629
|
+
return formatCommandInCwd(command, options.cwd);
|
|
1630
|
+
}
|
|
1631
|
+
function formatCommandInCwd(command, cwd) {
|
|
1632
|
+
return cwd ? `cd ${shellQuote(cwd)} && ${command}` : command;
|
|
1633
|
+
}
|
|
1634
|
+
function formatProShowCommand(taskId, sourceCli, options = {}) {
|
|
1635
|
+
return [`${formatCliCommand(sourceCli)} pro show ${shellQuote(taskId)}${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1636
|
+
.filter(Boolean)
|
|
1637
|
+
.join(" ");
|
|
1638
|
+
}
|
|
1639
|
+
function formatProLatestCommand(sourceCli, options = {}) {
|
|
1640
|
+
return [`${formatCliCommand(sourceCli)} pro latest${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1641
|
+
.filter(Boolean)
|
|
1642
|
+
.join(" ");
|
|
1643
|
+
}
|
|
1644
|
+
function formatResultResealCommand(taskId, sourceCli, options = {}) {
|
|
1645
|
+
return [
|
|
1646
|
+
`${formatCliCommand(sourceCli)} results reseal ${shellQuote(taskId)} --confirm-current-result`,
|
|
1647
|
+
options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined
|
|
1648
|
+
]
|
|
1649
|
+
.filter(Boolean)
|
|
1650
|
+
.join(" ");
|
|
1651
|
+
}
|
|
1652
|
+
function sourceAwareResultMessage(message, sourceCli, options = {}) {
|
|
1653
|
+
if (!sourceCli && !options.cwd)
|
|
1654
|
+
return message;
|
|
1655
|
+
return message.replace(/`prodex results reseal ([^`\s]+) --confirm-current-result`/g, (_match, taskId) => `\`${formatResultResealCommand(taskId, sourceCli, options)}\``);
|
|
1656
|
+
}
|
|
1657
|
+
function sourceAwareResultError(error, sourceCli, options = {}) {
|
|
1658
|
+
if (!isUntrustedResultError(error))
|
|
1659
|
+
return error;
|
|
1660
|
+
return new Error(sourceAwareResultMessage(errorMessage(error), sourceCli, options), { cause: error });
|
|
1661
|
+
}
|
|
1662
|
+
function formatBlockedConsultRecordedMessage(message, taskId, sourceCli, options = {}) {
|
|
1663
|
+
return `${message}\nblocked consult recorded: ${taskId}; inspect with \`${formatProShowCommand(taskId, sourceCli, options)}\` or \`${formatProLatestCommand(sourceCli, options)}\`.`;
|
|
1664
|
+
}
|
|
1665
|
+
function formatReleaseStatusCommand(sourceCli, options = {}) {
|
|
1666
|
+
return [`${formatCliCommand(sourceCli)} release status${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1667
|
+
.filter(Boolean)
|
|
1668
|
+
.join(" ");
|
|
1669
|
+
}
|
|
1670
|
+
function formatReleasePackCommand(sourceCli, options = {}) {
|
|
1671
|
+
return [
|
|
1672
|
+
`${formatCliCommand(sourceCli)} release pack${formatSourceCliOption(sourceCli)}`,
|
|
1673
|
+
options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined,
|
|
1674
|
+
"--pack-destination <dir>"
|
|
1675
|
+
]
|
|
1676
|
+
.filter(Boolean)
|
|
1677
|
+
.join(" ");
|
|
1678
|
+
}
|
|
1679
|
+
function formatGitPushUpstreamCommand(branch) {
|
|
1680
|
+
return `git push -u origin ${shellQuote(branch)}`;
|
|
1681
|
+
}
|
|
1682
|
+
function sourceAwareBrowserNextStep(nextStep, sourceCli, options = {}) {
|
|
1683
|
+
if (!nextStep)
|
|
1684
|
+
return nextStep;
|
|
1685
|
+
const targetRetry = nextStep.match(/^Open (https:\/\/chatgpt\.com\/\S+) in the (visible|dedicated) browser and retry(\. Current: .+|\.)$/);
|
|
1686
|
+
if (targetRetry) {
|
|
1687
|
+
const [, targetUrl, location, suffix] = targetRetry;
|
|
1688
|
+
return `Open ${targetUrl} in the ${location} browser and run \`${formatBrowserTargetAskCommand(sourceCli, {
|
|
1689
|
+
...options,
|
|
1690
|
+
targetUrl
|
|
1691
|
+
})}\`${suffix}`;
|
|
1692
|
+
}
|
|
1693
|
+
if (!sourceCli && !options.port && !options.cwd)
|
|
1694
|
+
return nextStep;
|
|
1695
|
+
return nextStep
|
|
1696
|
+
.replace(/`cd (.+?) && prodex pro browser login([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1697
|
+
return `\`cd ${cwdPrefix} && ${formatBrowserLoginCommandBody(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1698
|
+
})
|
|
1699
|
+
.replace(/`cd (.+?) && prodex pro browser smoke([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1700
|
+
return `\`cd ${cwdPrefix} && ${formatBrowserSmokeCommandBody(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1701
|
+
})
|
|
1702
|
+
.replace(/`cd (.+?) && prodex pro browser ask([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1703
|
+
return `\`cd ${cwdPrefix} && ${formatBrowserAskCommandBody(sourceCli)}${storedArgs ?? ""}\``;
|
|
1704
|
+
})
|
|
1705
|
+
.replace(/`prodex pro browser login([^`]*)?`/g, (_match, storedArgs) => {
|
|
1706
|
+
return `\`${formatBrowserLoginCommand(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1707
|
+
})
|
|
1708
|
+
.replace(/`prodex pro browser smoke([^`]*)?`/g, (_match, storedArgs) => {
|
|
1709
|
+
return `\`${formatBrowserSmokeCommand(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1710
|
+
})
|
|
1711
|
+
.replace(/`prodex pro browser ask([^`]*)?`/g, (_match, storedArgs) => {
|
|
1712
|
+
return `\`${formatBrowserAskCommandBody(sourceCli)}${storedArgs ?? ""}\``;
|
|
1713
|
+
})
|
|
1714
|
+
.replaceAll("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
|
|
1715
|
+
}
|
|
1716
|
+
function browserOptionsWithStoredPort(options, storedArgs) {
|
|
1717
|
+
if (options.port || !storedArgs)
|
|
1718
|
+
return options;
|
|
1719
|
+
const match = storedArgs.match(/(?:^|\s)--port\s+(\d{1,5})(?:\s|$)/);
|
|
1720
|
+
if (!match)
|
|
1721
|
+
return options;
|
|
1722
|
+
const port = Number(match[1]);
|
|
1723
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1724
|
+
return options;
|
|
1725
|
+
return { ...options, port };
|
|
1726
|
+
}
|
|
1727
|
+
function productCheckBrowserNextStep(nextStep, sourceCli, options = {}) {
|
|
1728
|
+
const sourceAware = sourceAwareBrowserNextStep(nextStep, sourceCli, options);
|
|
1729
|
+
if (!sourceAware)
|
|
1730
|
+
return sourceAware;
|
|
1731
|
+
if (sourceAware.includes("`"))
|
|
1732
|
+
return sourceAware;
|
|
1733
|
+
if (sourceAware.includes("pass --target-url with --confirm-target")) {
|
|
1734
|
+
return sourceAware.replace("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
|
|
1735
|
+
}
|
|
1736
|
+
return sourceAware.replace(/(?:and|then) retry\.$/, `then run \`${formatBrowserSmokeCommand(sourceCli, options)}\`.`);
|
|
1737
|
+
}
|
|
1738
|
+
function sourceAwareBrowserBlocker(blocker, sourceCli, options = {}) {
|
|
1739
|
+
const nextStep = sourceAwareBrowserNextStep(blocker.next_step, sourceCli, options);
|
|
1740
|
+
return nextStep === blocker.next_step ? blocker : { ...blocker, next_step: nextStep };
|
|
1741
|
+
}
|
|
1742
|
+
function sourceAwareSetupMessage(message, sourceCli, options = {}) {
|
|
1743
|
+
if (!sourceCli && !options.cwd)
|
|
1744
|
+
return message;
|
|
1745
|
+
const setupCommand = formatSetupCommand(sourceCli, options);
|
|
1746
|
+
return message
|
|
1747
|
+
.replaceAll("`prodex setup --token-ttl-hours <hours>`", `\`${setupCommand} --token-ttl-hours <hours>\``)
|
|
1748
|
+
.replaceAll("`prodex setup`", `\`${setupCommand}\``);
|
|
1749
|
+
}
|
|
1750
|
+
function sourceAwareReleaseMessage(message, sourceCli, options = {}) {
|
|
1751
|
+
if (!sourceCli && !options.cwd)
|
|
1752
|
+
return message;
|
|
1753
|
+
return message
|
|
1754
|
+
.replaceAll("`prodex release pack --pack-destination <dir>`", `\`${formatReleasePackCommand(sourceCli, options)}\``)
|
|
1755
|
+
.replaceAll("`prodex release status`", `\`${formatReleaseStatusCommand(sourceCli, options)}\``);
|
|
1756
|
+
}
|
|
1757
|
+
async function formatReleaseStatus(cwd, sourceCli, releaseHintCwd) {
|
|
1758
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
1759
|
+
const raw = await readReleasePackageJson(packageJsonPath).catch(async (error) => {
|
|
1760
|
+
if (!isMissingFileError(error))
|
|
1761
|
+
throw error;
|
|
1762
|
+
return undefined;
|
|
1763
|
+
});
|
|
1764
|
+
if (raw === undefined) {
|
|
1765
|
+
const lines = [formatReleaseStatusCommand(sourceCli, { cwd: releaseHintCwd }), "package: <missing package.json>"];
|
|
1766
|
+
lines.push(`metadata: blocked package.json not found at ${packageJsonPath}`);
|
|
1767
|
+
const gitStatus = await readReleaseGitStatus(cwd);
|
|
1768
|
+
lines.push(gitStatus.line);
|
|
1769
|
+
if (gitStatus.next)
|
|
1770
|
+
lines.push(`git_next: ${gitStatus.next}`);
|
|
1771
|
+
lines.push("next: run this command from a package root or pass `--cwd /absolute/path/to/repo`");
|
|
1772
|
+
lines.push("verification: run `npm run release:verify` anytime without weakening the publish guard");
|
|
1773
|
+
return lines.join("\n");
|
|
1774
|
+
}
|
|
1775
|
+
let packageJson;
|
|
1776
|
+
try {
|
|
1777
|
+
packageJson = JSON.parse(raw);
|
|
1778
|
+
}
|
|
1779
|
+
catch {
|
|
1780
|
+
const lines = [formatReleaseStatusCommand(sourceCli, { cwd: releaseHintCwd }), "package: <invalid package.json>"];
|
|
1781
|
+
lines.push(`metadata: blocked package.json is not valid JSON at ${packageJsonPath}`);
|
|
1782
|
+
const gitStatus = await readReleaseGitStatus(cwd);
|
|
1783
|
+
lines.push(gitStatus.line);
|
|
1784
|
+
if (gitStatus.next)
|
|
1785
|
+
lines.push(`git_next: ${gitStatus.next}`);
|
|
1786
|
+
lines.push("next: fix package.json syntax, then run `npm run release:check`");
|
|
1787
|
+
lines.push("verification: run `npm run release:verify` anytime without weakening the publish guard");
|
|
1788
|
+
return lines.join("\n");
|
|
1789
|
+
}
|
|
1790
|
+
const name = typeof packageJson.name === "string" && packageJson.name.trim() ? packageJson.name : "<unnamed>";
|
|
1791
|
+
const version = typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "<unversioned>";
|
|
1792
|
+
const lines = [formatReleaseStatusCommand(sourceCli, { cwd: releaseHintCwd }), `package: ${name}@${version}`];
|
|
1793
|
+
const license = typeof packageJson.license === "string" ? packageJson.license.trim() : "";
|
|
1794
|
+
const identityError = packageIdentityError(packageJson);
|
|
1795
|
+
let metadataNext = "run `npm run release:check` before publishing";
|
|
1796
|
+
let metadataReady = false;
|
|
1797
|
+
let packReady = false;
|
|
1798
|
+
const packCheckEligible = !identityError;
|
|
1799
|
+
if (identityError) {
|
|
1800
|
+
lines.push(`metadata: blocked ${identityError.message}`);
|
|
1801
|
+
metadataNext = identityError.next;
|
|
1802
|
+
}
|
|
1803
|
+
else if (packageJson.private === true) {
|
|
1804
|
+
lines.push("metadata: blocked package.json private: true prevents npm publish");
|
|
1805
|
+
metadataNext = "remove `private: true` before public publishing, then run `npm run release:check`";
|
|
1806
|
+
}
|
|
1807
|
+
else {
|
|
1808
|
+
if (!license) {
|
|
1809
|
+
lines.push("metadata: blocked package.json must include an explicit license before publishing");
|
|
1810
|
+
metadataNext = await missingPackageLicenseNextStep(cwd);
|
|
1811
|
+
}
|
|
1812
|
+
else if (license === "UNLICENSED") {
|
|
1813
|
+
lines.push('metadata: blocked license "UNLICENSED" is not publishable');
|
|
1814
|
+
metadataNext = "choose a public license and add LICENSE, then run `npm run release:check`";
|
|
1815
|
+
}
|
|
1816
|
+
else if (license !== "MIT") {
|
|
1817
|
+
lines.push(`metadata: blocked license=${license} package.json license must be MIT before publishing`);
|
|
1818
|
+
metadataNext = "set package.json license to MIT and use the MIT LICENSE text, then run `npm run release:check`";
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
const licenseFile = await readLicenseFileStatus(path.join(cwd, "LICENSE"), license);
|
|
1822
|
+
if (licenseFile.status === "missing") {
|
|
1823
|
+
lines.push(`metadata: blocked license=${license} license_file=missing`);
|
|
1824
|
+
metadataNext = "add LICENSE, then run `npm run release:check`";
|
|
1825
|
+
}
|
|
1826
|
+
else if (licenseFile.status === "invalid") {
|
|
1827
|
+
lines.push(`metadata: blocked license=${license} license_file=invalid - LICENSE must be a regular file and must not be a symlink`);
|
|
1828
|
+
metadataNext = "replace LICENSE with a regular file, then run `npm run release:check`";
|
|
1829
|
+
}
|
|
1830
|
+
else if (licenseFile.status === "hardlinked") {
|
|
1831
|
+
lines.push(`metadata: blocked license=${license} license_file=invalid - LICENSE must not have hard links`);
|
|
1832
|
+
metadataNext = "replace LICENSE with a non-hard-linked regular file, then run `npm run release:check`";
|
|
1833
|
+
}
|
|
1834
|
+
else if (licenseFile.status === "mismatch") {
|
|
1835
|
+
lines.push(`metadata: blocked license=${license} license_file=mismatch - LICENSE content must match package.json license MIT`);
|
|
1836
|
+
metadataNext = "replace LICENSE with the MIT LICENSE text, then run `npm run release:check`";
|
|
1837
|
+
}
|
|
1838
|
+
else {
|
|
1839
|
+
lines.push(`metadata: ok license=${license} license_file=present`);
|
|
1840
|
+
metadataReady = true;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
if (packCheckEligible) {
|
|
1845
|
+
const packStatus = await readReleasePackStatus(cwd, packageJson, sourceCli, releaseHintCwd);
|
|
1846
|
+
lines.push(packStatus.line);
|
|
1847
|
+
if (packStatus.next) {
|
|
1848
|
+
if (metadataReady) {
|
|
1849
|
+
metadataNext = packStatus.next;
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
lines.push(`pack_next: ${packStatus.next}`);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
packReady = true;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const gitStatus = await readReleaseGitStatus(cwd);
|
|
1860
|
+
lines.push(gitStatus.line);
|
|
1861
|
+
if (gitStatus.next)
|
|
1862
|
+
lines.push(`git_next: ${gitStatus.next}`);
|
|
1863
|
+
if (metadataReady && packReady && !gitStatus.next) {
|
|
1864
|
+
metadataNext = "run `prodex release pack --pack-destination <dir>`, then run the printed release_pack_verify dry-run before npm publish";
|
|
1865
|
+
}
|
|
1866
|
+
lines.push(`next: ${sourceAwareReleaseMessage(metadataNext, sourceCli, { cwd: releaseHintCwd })}`);
|
|
1867
|
+
lines.push("verification: run `npm run release:verify` anytime without weakening the publish guard");
|
|
1868
|
+
return lines.join("\n");
|
|
1869
|
+
}
|
|
1870
|
+
function packageIdentityError(packageJson) {
|
|
1871
|
+
if (!isNonEmptyPackageString(packageJson.name) || !isNonEmptyPackageString(packageJson.version)) {
|
|
1872
|
+
return {
|
|
1873
|
+
message: "package.json must include non-empty string name and version",
|
|
1874
|
+
next: "set package.json name and version, then run `npm run release:check`"
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
if (!isNpmPublishablePackageName(packageJson.name)) {
|
|
1878
|
+
return {
|
|
1879
|
+
message: "package.json name must be npm-publishable",
|
|
1880
|
+
next: "fix package.json name, then run `npm run release:check`"
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
if (!isValidSemverVersion(packageJson.version)) {
|
|
1884
|
+
return {
|
|
1885
|
+
message: "package.json version must be valid semver",
|
|
1886
|
+
next: "fix package.json version, then run `npm run release:check`"
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
return undefined;
|
|
1890
|
+
}
|
|
1891
|
+
async function missingPackageLicenseNextStep(cwd) {
|
|
1892
|
+
const licenseFile = await readLicenseFileStatus(path.join(cwd, "LICENSE"));
|
|
1893
|
+
if (licenseFile.status === "present") {
|
|
1894
|
+
return "choose a license and set package.json license, then run `npm run release:check`";
|
|
1895
|
+
}
|
|
1896
|
+
if (licenseFile.status === "invalid") {
|
|
1897
|
+
return "choose a license and replace LICENSE with a regular file, then run `npm run release:check`";
|
|
1898
|
+
}
|
|
1899
|
+
if (licenseFile.status === "hardlinked") {
|
|
1900
|
+
return "choose a license and replace LICENSE with a non-hard-linked regular file, then run `npm run release:check`";
|
|
1901
|
+
}
|
|
1902
|
+
return "choose a license, add LICENSE, then run `npm run release:check`";
|
|
1903
|
+
}
|
|
1904
|
+
function isNonEmptyPackageString(value) {
|
|
1905
|
+
return typeof value === "string" && value.trim() !== "";
|
|
1906
|
+
}
|
|
1907
|
+
function isNpmPublishablePackageName(value) {
|
|
1908
|
+
if (!isNonEmptyPackageString(value) || value.length > 214 || value !== value.toLowerCase())
|
|
1909
|
+
return false;
|
|
1910
|
+
if (RESERVED_PACKAGE_NAMES.has(value))
|
|
1911
|
+
return false;
|
|
1912
|
+
if (value.startsWith("@")) {
|
|
1913
|
+
const parts = value.slice(1).split("/");
|
|
1914
|
+
return parts.length === 2 && parts.every(isPackageNameSegment);
|
|
1915
|
+
}
|
|
1916
|
+
return !value.includes("/") && isPackageNameSegment(value);
|
|
1917
|
+
}
|
|
1918
|
+
function isPackageNameSegment(value) {
|
|
1919
|
+
return /^(?![._])[a-z0-9][a-z0-9._~-]*$/.test(value);
|
|
1920
|
+
}
|
|
1921
|
+
function isValidSemverVersion(value) {
|
|
1922
|
+
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(value);
|
|
1923
|
+
}
|
|
1924
|
+
async function readReleasePackageJson(packageJsonPath) {
|
|
1925
|
+
return readFile(packageJsonPath, "utf8");
|
|
1926
|
+
}
|
|
1927
|
+
async function readReleasePackStatus(cwd, packageJson, sourceCli, releaseHintCwd) {
|
|
1928
|
+
try {
|
|
1929
|
+
const { stdout } = await execFileAsync(commandForPlatform("npm"), ["pack", "--json", "--dry-run", "--ignore-scripts"], {
|
|
1930
|
+
cwd,
|
|
1931
|
+
timeout: 120_000,
|
|
1932
|
+
maxBuffer: 20 * 1024 * 1024
|
|
1933
|
+
});
|
|
1934
|
+
const files = parsePackedFiles(stdout);
|
|
1935
|
+
const nonRegular = await findNonRegularPackedFiles(cwd, files);
|
|
1936
|
+
if (nonRegular.length > 0) {
|
|
1937
|
+
return {
|
|
1938
|
+
line: `pack: blocked packed files must be regular non-symlink files: ${formatPathList(nonRegular)}`,
|
|
1939
|
+
next: "replace non-regular or symlinked packed files with regular files, then run `npm run release:check`"
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
const invalid = findExecutableNonBinPackedFiles(files, packageJson);
|
|
1943
|
+
if (invalid.length > 0) {
|
|
1944
|
+
return {
|
|
1945
|
+
line: `pack: blocked packed files have unexpected executable modes outside package bin entries: ${formatPathList(invalid)}`,
|
|
1946
|
+
next: sourceAwareReleaseMessage("fix file modes or publish from a filesystem that preserves executable bits, then run `npm run release:check`; on WSL/Windows mounts, create a sanitized tarball with `prodex release pack --pack-destination <dir>` after `npm run release:verify`; release pack prints `npm publish --dry-run <tarball>` and warns that tarball publish bypasses prepublishOnly before printing `npm publish <tarball>`", sourceCli, { cwd: releaseHintCwd })
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
const hardLinked = await findHardLinkedPackedFiles(cwd, files);
|
|
1950
|
+
if (hardLinked.length > 0) {
|
|
1951
|
+
return {
|
|
1952
|
+
line: `pack: blocked packed files have hard links: ${formatPathList(hardLinked)}`,
|
|
1953
|
+
next: "replace hard-linked packed files with independent files, then run `npm run release:check`"
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
return { line: "pack: ok file_modes=ok" };
|
|
1957
|
+
}
|
|
1958
|
+
catch (error) {
|
|
1959
|
+
return {
|
|
1960
|
+
line: `pack: blocked npm pack dry-run failed: ${firstErrorLine(error)}`,
|
|
1961
|
+
next: "fix npm pack dry-run, then run `npm run release:check`"
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function parsePackedFiles(stdout) {
|
|
1966
|
+
let entries;
|
|
1967
|
+
try {
|
|
1968
|
+
entries = JSON.parse(stdout);
|
|
1969
|
+
}
|
|
1970
|
+
catch {
|
|
1971
|
+
throw new Error("npm pack dry-run did not return valid JSON");
|
|
1972
|
+
}
|
|
1973
|
+
const files = entries?.[0]?.files;
|
|
1974
|
+
if (!Array.isArray(files)) {
|
|
1975
|
+
throw new Error("npm pack dry-run did not return a file list");
|
|
1976
|
+
}
|
|
1977
|
+
for (const file of files) {
|
|
1978
|
+
if (typeof file?.path !== "string" || file.path.trim() === "") {
|
|
1979
|
+
throw new Error("npm pack dry-run file entry is missing a path");
|
|
1980
|
+
}
|
|
1981
|
+
if (typeof file.mode !== "number") {
|
|
1982
|
+
throw new Error(`npm pack dry-run file entry is missing mode metadata: ${normalizePackagePath(file.path)}`);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return files;
|
|
1986
|
+
}
|
|
1987
|
+
function findExecutableNonBinPackedFiles(files, packageJson) {
|
|
1988
|
+
const binPaths = packageBinPaths(packageJson.bin);
|
|
1989
|
+
return files
|
|
1990
|
+
.filter((file) => (file.mode & 0o111) !== 0)
|
|
1991
|
+
.map((file) => normalizePackagePath(file.path))
|
|
1992
|
+
.filter((filePath) => !binPaths.has(filePath));
|
|
1993
|
+
}
|
|
1994
|
+
async function findNonRegularPackedFiles(cwd, files) {
|
|
1995
|
+
const invalid = [];
|
|
1996
|
+
for (const file of files) {
|
|
1997
|
+
const packagePath = normalizePackagePath(file.path);
|
|
1998
|
+
const filePath = path.join(cwd, packagePath);
|
|
1999
|
+
const relative = path.relative(cwd, filePath);
|
|
2000
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
2001
|
+
invalid.push(packagePath);
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
try {
|
|
2005
|
+
const stat = await lstat(filePath);
|
|
2006
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
2007
|
+
invalid.push(packagePath);
|
|
2008
|
+
}
|
|
2009
|
+
catch (error) {
|
|
2010
|
+
if (isMissingFileError(error))
|
|
2011
|
+
invalid.push(packagePath);
|
|
2012
|
+
else
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return invalid;
|
|
2017
|
+
}
|
|
2018
|
+
async function findHardLinkedPackedFiles(cwd, files) {
|
|
2019
|
+
const invalid = [];
|
|
2020
|
+
for (const file of files) {
|
|
2021
|
+
const packagePath = normalizePackagePath(file.path);
|
|
2022
|
+
const filePath = path.join(cwd, packagePath);
|
|
2023
|
+
const relative = path.relative(cwd, filePath);
|
|
2024
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
2025
|
+
invalid.push(packagePath);
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
try {
|
|
2029
|
+
const stat = await lstat(filePath);
|
|
2030
|
+
if (stat.nlink > 1)
|
|
2031
|
+
invalid.push(packagePath);
|
|
2032
|
+
}
|
|
2033
|
+
catch (error) {
|
|
2034
|
+
if (isMissingFileError(error))
|
|
2035
|
+
invalid.push(packagePath);
|
|
2036
|
+
else
|
|
2037
|
+
throw error;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return invalid;
|
|
2041
|
+
}
|
|
2042
|
+
function packageBinPaths(bin) {
|
|
2043
|
+
const paths = new Set();
|
|
2044
|
+
if (typeof bin === "string") {
|
|
2045
|
+
paths.add(normalizePackagePath(bin));
|
|
2046
|
+
}
|
|
2047
|
+
else if (typeof bin === "object" && bin !== null) {
|
|
2048
|
+
for (const value of Object.values(bin)) {
|
|
2049
|
+
if (typeof value === "string")
|
|
2050
|
+
paths.add(normalizePackagePath(value));
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return paths;
|
|
2054
|
+
}
|
|
2055
|
+
function normalizePackagePath(value) {
|
|
2056
|
+
return value.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
2057
|
+
}
|
|
2058
|
+
function formatPathList(paths) {
|
|
2059
|
+
const shown = paths.slice(0, 8).join(", ");
|
|
2060
|
+
return paths.length > 8 ? `${shown}, ... (${paths.length} files)` : shown;
|
|
2061
|
+
}
|
|
2062
|
+
async function runReleasePackCommand(input) {
|
|
2063
|
+
const scriptPath = path.join(packageRoot, "scripts", "release-pack.mjs");
|
|
2064
|
+
const args = [scriptPath, "--root", input.packageRoot, "--pack-destination", input.packDestination];
|
|
2065
|
+
if (input.keepWorkdir)
|
|
2066
|
+
args.push("--keep-workdir");
|
|
2067
|
+
try {
|
|
2068
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, args, {
|
|
2069
|
+
cwd: input.cwd,
|
|
2070
|
+
timeout: 120_000,
|
|
2071
|
+
maxBuffer: 20 * 1024 * 1024
|
|
2072
|
+
});
|
|
2073
|
+
writeCommandOutput(sourceAwareReleaseMessage(stdout, input.sourceCli, { cwd: input.releaseStatusCwd }), input.stdout);
|
|
2074
|
+
writeCommandOutput(stderr, input.stderr);
|
|
2075
|
+
}
|
|
2076
|
+
catch (error) {
|
|
2077
|
+
throw new Error(firstErrorLine(error));
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function writeCommandOutput(output, write) {
|
|
2081
|
+
const trimmed = output.replace(/\r?\n$/, "");
|
|
2082
|
+
if (!trimmed)
|
|
2083
|
+
return;
|
|
2084
|
+
for (const line of trimmed.split(/\r?\n/))
|
|
2085
|
+
write(line);
|
|
2086
|
+
}
|
|
2087
|
+
function commandForPlatform(command) {
|
|
2088
|
+
return process.platform === "win32" && command === "npm" ? "npm.cmd" : command;
|
|
2089
|
+
}
|
|
2090
|
+
function firstErrorLine(error) {
|
|
2091
|
+
const failed = typeof error === "object" && error !== null ? error : {};
|
|
2092
|
+
const stderr = firstOutputLine(failed.stderr);
|
|
2093
|
+
if (stderr)
|
|
2094
|
+
return stderr;
|
|
2095
|
+
const stdout = firstOutputLine(failed.stdout);
|
|
2096
|
+
if (stdout)
|
|
2097
|
+
return stdout;
|
|
2098
|
+
if (typeof failed.code === "number")
|
|
2099
|
+
return `exit code ${failed.code}`;
|
|
2100
|
+
if (typeof failed.signal === "string" && failed.signal) {
|
|
2101
|
+
return `signal ${failed.signal}`;
|
|
2102
|
+
}
|
|
2103
|
+
return errorMessage(error).split(/\r?\n/)[0];
|
|
2104
|
+
}
|
|
2105
|
+
function firstOutputLine(value) {
|
|
2106
|
+
if (typeof value !== "string")
|
|
2107
|
+
return undefined;
|
|
2108
|
+
return value
|
|
2109
|
+
.split(/\r?\n/)
|
|
2110
|
+
.map((line) => line.trim())
|
|
2111
|
+
.find(Boolean);
|
|
2112
|
+
}
|
|
2113
|
+
async function readReleaseGitStatus(cwd) {
|
|
2114
|
+
try {
|
|
2115
|
+
const insideWorkTree = (await gitStdout(cwd, ["rev-parse", "--is-inside-work-tree"])).trim();
|
|
2116
|
+
if (insideWorkTree !== "true") {
|
|
2117
|
+
return {
|
|
2118
|
+
line: "git: blocked not a git worktree",
|
|
2119
|
+
next: "initialize a git repo and commit the release state before public release"
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
catch {
|
|
2124
|
+
return {
|
|
2125
|
+
line: "git: blocked not a git worktree",
|
|
2126
|
+
next: "initialize a git repo and commit the release state before public release"
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
const [branch, commit, statusOutput, branchStatusOutput, remoteOutput, upstream] = await Promise.all([
|
|
2130
|
+
gitStdout(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]).then((value) => value.trim() || "unknown", () => "unknown"),
|
|
2131
|
+
gitStdout(cwd, ["rev-parse", "--short", "HEAD"]).then((value) => value.trim() || "unknown", () => "unknown"),
|
|
2132
|
+
gitStdout(cwd, ["status", "--porcelain"]).then((value) => value.trim(), () => ""),
|
|
2133
|
+
gitStdout(cwd, ["status", "--porcelain=v1", "--branch"]).then((value) => value.trim(), () => ""),
|
|
2134
|
+
gitStdout(cwd, ["remote"]).then((value) => value.trim(), () => ""),
|
|
2135
|
+
gitStdout(cwd, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]).then((value) => value.trim(), () => "")
|
|
2136
|
+
]);
|
|
2137
|
+
const dirtyCount = statusOutput ? statusOutput.split(/\r?\n/).filter(Boolean).length : 0;
|
|
2138
|
+
const remotes = remoteOutput ? remoteOutput.split(/\r?\n/).map((remote) => remote.trim()).filter(Boolean) : [];
|
|
2139
|
+
const remoteText = remotes.length > 0 ? remotes.join(",") : "none";
|
|
2140
|
+
const gitContext = `branch=${branch} commit=${commit}`;
|
|
2141
|
+
if (dirtyCount > 0) {
|
|
2142
|
+
return {
|
|
2143
|
+
line: `git: blocked worktree has uncommitted changes files=${dirtyCount} ${gitContext} remote=${remoteText}`,
|
|
2144
|
+
next: "commit or stash local changes before release"
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
if (branch === "HEAD") {
|
|
2148
|
+
return {
|
|
2149
|
+
line: `git: blocked detached HEAD commit=${commit} remote=${remoteText}`,
|
|
2150
|
+
next: "check out a release branch before public release"
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
if (remotes.length === 0) {
|
|
2154
|
+
return {
|
|
2155
|
+
line: `git: blocked no remote configured ${gitContext}`,
|
|
2156
|
+
next: `add a remote, then push with upstream tracking: git remote add origin <git-url>; ${formatGitPushUpstreamCommand(branch)}`
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
const relation = parseGitBranchRelation(branchStatusOutput);
|
|
2160
|
+
const effectiveUpstream = upstream || relation.upstream || "";
|
|
2161
|
+
if (!upstream && relation.gone && relation.upstream) {
|
|
2162
|
+
return {
|
|
2163
|
+
line: `git: blocked upstream is gone ${gitContext} remote=${remoteText} upstream=${relation.upstream}`,
|
|
2164
|
+
next: "restore upstream tracking before public release"
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
if (!effectiveUpstream) {
|
|
2168
|
+
return {
|
|
2169
|
+
line: `git: blocked no upstream configured ${gitContext} remote=${remoteText}`,
|
|
2170
|
+
next: `push the branch with upstream tracking: ${formatGitPushUpstreamCommand(branch)}`
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
if (relation.gone) {
|
|
2174
|
+
return {
|
|
2175
|
+
line: `git: blocked upstream is gone ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
2176
|
+
next: "restore upstream tracking before public release"
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
if (relation.ahead > 0 && relation.behind > 0) {
|
|
2180
|
+
return {
|
|
2181
|
+
line: `git: blocked branch diverged ahead=${relation.ahead} behind=${relation.behind} ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
2182
|
+
next: "sync the branch with upstream before public release"
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
if (relation.ahead > 0) {
|
|
2186
|
+
return {
|
|
2187
|
+
line: `git: blocked branch has unpushed commits ahead=${relation.ahead} ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
2188
|
+
next: "push local commits before public release"
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
if (relation.behind > 0) {
|
|
2192
|
+
return {
|
|
2193
|
+
line: `git: blocked branch is behind upstream behind=${relation.behind} ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`,
|
|
2194
|
+
next: "sync the branch with upstream before public release"
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
return {
|
|
2198
|
+
line: `git: ok ${gitContext} remote=${remoteText} upstream=${effectiveUpstream}`
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
function parseGitBranchRelation(statusOutput) {
|
|
2202
|
+
const branchLine = statusOutput.split(/\r?\n/).find((line) => line.startsWith("## ")) ?? "";
|
|
2203
|
+
const relationText = /\[([^\]]+)\]/.exec(branchLine)?.[1] ?? "";
|
|
2204
|
+
const upstream = /\.\.\.([^\s\[]+)/.exec(branchLine)?.[1];
|
|
2205
|
+
const ahead = Number(/\bahead (\d+)\b/.exec(relationText)?.[1] ?? 0);
|
|
2206
|
+
const behind = Number(/\bbehind (\d+)\b/.exec(relationText)?.[1] ?? 0);
|
|
2207
|
+
return { ahead, behind, gone: /\bgone\b/.test(relationText), upstream };
|
|
2208
|
+
}
|
|
2209
|
+
async function gitStdout(cwd, args) {
|
|
2210
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
2211
|
+
return stdout;
|
|
2212
|
+
}
|
|
2213
|
+
async function readLicenseFileStatus(filePath, expectedLicense) {
|
|
2214
|
+
try {
|
|
2215
|
+
const stat = await lstat(filePath);
|
|
2216
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
2217
|
+
return { status: "invalid" };
|
|
2218
|
+
if (stat.nlink > 1)
|
|
2219
|
+
return { status: "hardlinked" };
|
|
2220
|
+
const raw = await readFile(filePath, "utf8");
|
|
2221
|
+
if (expectedLicense === "MIT" && !isMitLicenseText(raw))
|
|
2222
|
+
return { status: "mismatch" };
|
|
2223
|
+
return { status: "present" };
|
|
2224
|
+
}
|
|
2225
|
+
catch (error) {
|
|
2226
|
+
if (isMissingFileError(error))
|
|
2227
|
+
return { status: "missing" };
|
|
2228
|
+
throw error;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function isMitLicenseText(raw) {
|
|
2232
|
+
return /\bMIT License\b/.test(raw);
|
|
2233
|
+
}
|
|
2234
|
+
async function runDoctor(store, io, sourceCli, setupHintCwd) {
|
|
2235
|
+
let ok = true;
|
|
2236
|
+
io.stdout("prodex doctor");
|
|
2237
|
+
try {
|
|
2238
|
+
const bridgeReady = await store.hasReadyBridgeStorageReadOnly();
|
|
2239
|
+
io.stdout(bridgeReady
|
|
2240
|
+
? "bridge: ok (.bridge)"
|
|
2241
|
+
: `bridge: missing/incomplete (.bridge) - run \`${formatInitCommand(sourceCli, { cwd: setupHintCwd })}\` when you need local task/result storage`);
|
|
2242
|
+
}
|
|
2243
|
+
catch (error) {
|
|
2244
|
+
ok = false;
|
|
2245
|
+
io.stdout(`bridge: failed ${errorMessage(error)}`);
|
|
2246
|
+
}
|
|
2247
|
+
try {
|
|
2248
|
+
const config = await loadLocalConfig(io.cwd);
|
|
2249
|
+
const tokenStatus = getTokenExpiryStatus(config);
|
|
2250
|
+
if (tokenStatus.status === "expired") {
|
|
2251
|
+
ok = false;
|
|
2252
|
+
io.stdout(`config: failed token expired at ${tokenStatus.token_expires_at} - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2253
|
+
}
|
|
2254
|
+
else {
|
|
2255
|
+
io.stdout(`config: ok ${redactServerUrl(config.server_url)} token_status=${tokenStatus.status}`);
|
|
2256
|
+
const warningLine = formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd);
|
|
2257
|
+
if (warningLine)
|
|
2258
|
+
io.stdout(warningLine);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch (error) {
|
|
2262
|
+
if (isMissingFileError(error)) {
|
|
2263
|
+
io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2264
|
+
}
|
|
2265
|
+
else {
|
|
2266
|
+
ok = false;
|
|
2267
|
+
io.stdout(`config: failed ${sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd })}`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
try {
|
|
2271
|
+
const smoke = await runMcpWriteSmoke();
|
|
2272
|
+
io.stdout(`mcp_write_smoke: ok path=${smoke.path} receipt_payload=${smoke.receipt_payload} staged=${smoke.staged}`);
|
|
2273
|
+
}
|
|
2274
|
+
catch (error) {
|
|
2275
|
+
ok = false;
|
|
2276
|
+
io.stdout(`mcp_write_smoke: failed ${errorMessage(error)}`);
|
|
2277
|
+
}
|
|
2278
|
+
try {
|
|
2279
|
+
const smoke = await runHttpMcpCatalogSmoke();
|
|
2280
|
+
io.stdout(`http_mcp_smoke: ok task_flow=${smoke.taskFlow} finalizers=${smoke.finalizers} search=${smoke.search} tools=${smoke.tools.join(",")}`);
|
|
2281
|
+
}
|
|
2282
|
+
catch (error) {
|
|
2283
|
+
ok = false;
|
|
2284
|
+
io.stdout(`http_mcp_smoke: failed ${errorMessage(error)}`);
|
|
2285
|
+
}
|
|
2286
|
+
return ok ? 0 : 1;
|
|
2287
|
+
}
|
|
2288
|
+
async function runHttpMcpCatalogSmoke() {
|
|
2289
|
+
const cwd = await mkdtemp(path.join(tmpdir(), "prodex-http-doctor-"));
|
|
2290
|
+
let running;
|
|
2291
|
+
let client;
|
|
2292
|
+
let smokeFailed = false;
|
|
2293
|
+
try {
|
|
2294
|
+
await writeFile(path.join(cwd, "search-smoke.txt"), "before\n--doctor-rg-literal ok\nafter\n", "utf8");
|
|
2295
|
+
running = await startHttpMcpServer({
|
|
2296
|
+
cwd,
|
|
2297
|
+
host: "127.0.0.1",
|
|
2298
|
+
port: 0,
|
|
2299
|
+
token: "doctor-token"
|
|
2300
|
+
});
|
|
2301
|
+
client = new Client({ name: "prodex-doctor", version: CLI_VERSION });
|
|
2302
|
+
await withTimeout(client.connect(new StreamableHTTPClientTransport(new URL(running.mcp_url))), 20_000, "timed out connecting to HTTP MCP server");
|
|
2303
|
+
const result = await withTimeout(client.listTools(), 20_000, "timed out listing HTTP MCP tools");
|
|
2304
|
+
const names = result.tools.map((tool) => tool.name);
|
|
2305
|
+
const missing = DOCTOR_REQUIRED_MCP_TOOLS.filter((tool) => !names.includes(tool));
|
|
2306
|
+
if (missing.length > 0)
|
|
2307
|
+
throw new Error(`missing MCP tools: ${missing.join(",")}`);
|
|
2308
|
+
await runHttpMcpSearchSmoke(client);
|
|
2309
|
+
await runHttpMcpFinalizerSmoke(client);
|
|
2310
|
+
return { tools: [...DOCTOR_REQUIRED_MCP_TOOLS], taskFlow: "ok", finalizers: "ok", search: "ok" };
|
|
2311
|
+
}
|
|
2312
|
+
catch (error) {
|
|
2313
|
+
smokeFailed = true;
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
finally {
|
|
2317
|
+
const cleanupErrors = [];
|
|
2318
|
+
if (client) {
|
|
2319
|
+
try {
|
|
2320
|
+
await withTimeout(client.close(), 10_000, "timed out closing HTTP MCP client");
|
|
2321
|
+
}
|
|
2322
|
+
catch (error) {
|
|
2323
|
+
cleanupErrors.push(errorMessage(error));
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (running) {
|
|
2327
|
+
try {
|
|
2328
|
+
await running.close({ forceAfterMs: 1_000, timeoutMs: 10_000 });
|
|
2329
|
+
}
|
|
2330
|
+
catch (error) {
|
|
2331
|
+
cleanupErrors.push(errorMessage(error));
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
try {
|
|
2335
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2336
|
+
}
|
|
2337
|
+
catch (error) {
|
|
2338
|
+
cleanupErrors.push(errorMessage(error));
|
|
2339
|
+
}
|
|
2340
|
+
if (cleanupErrors.length > 0 && !smokeFailed) {
|
|
2341
|
+
throw new Error(`HTTP MCP smoke cleanup failed: ${cleanupErrors.join("; ")}`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
async function runHttpMcpSearchSmoke(client) {
|
|
2346
|
+
const result = await callHttpMcpJsonTool(client, "repo_search", {
|
|
2347
|
+
query: "--doctor-rg-literal"
|
|
2348
|
+
});
|
|
2349
|
+
if (result.matches.length !== 1 ||
|
|
2350
|
+
result.matches[0]?.path !== "search-smoke.txt" ||
|
|
2351
|
+
result.matches[0]?.line !== 2 ||
|
|
2352
|
+
result.matches[0]?.text !== "--doctor-rg-literal ok") {
|
|
2353
|
+
throw new Error(`unexpected HTTP MCP search result: ${JSON.stringify(result)}`);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
async function runHttpMcpFinalizerSmoke(client) {
|
|
2357
|
+
const doneTask = await callHttpMcpJsonTool(client, "bridge_create_task", {
|
|
2358
|
+
title: "Doctor HTTP complete smoke",
|
|
2359
|
+
prompt: "Complete this task over HTTP MCP"
|
|
2360
|
+
});
|
|
2361
|
+
assertDoctorMcpTask(doneTask.task, {
|
|
2362
|
+
taskId: doneTask.task.id,
|
|
2363
|
+
status: "new",
|
|
2364
|
+
title: "Doctor HTTP complete smoke"
|
|
2365
|
+
});
|
|
2366
|
+
const fetchedTask = await callHttpMcpJsonTool(client, "bridge_get_task", {
|
|
2367
|
+
task_id: doneTask.task.id
|
|
2368
|
+
});
|
|
2369
|
+
const newTasks = await callHttpMcpJsonTool(client, "bridge_list_tasks", {
|
|
2370
|
+
status: "new"
|
|
2371
|
+
});
|
|
2372
|
+
assertDoctorMcpTask(fetchedTask.task, {
|
|
2373
|
+
taskId: doneTask.task.id,
|
|
2374
|
+
status: "new",
|
|
2375
|
+
title: "Doctor HTTP complete smoke"
|
|
2376
|
+
});
|
|
2377
|
+
assertDoctorMcpTaskInList(newTasks.tasks, {
|
|
2378
|
+
taskId: doneTask.task.id,
|
|
2379
|
+
status: "new"
|
|
2380
|
+
});
|
|
2381
|
+
const claimedTask = await callHttpMcpJsonTool(client, "bridge_claim_task", {
|
|
2382
|
+
task_id: doneTask.task.id,
|
|
2383
|
+
claimed_by: "doctor-http-smoke"
|
|
2384
|
+
});
|
|
2385
|
+
const claimedTasks = await callHttpMcpJsonTool(client, "bridge_list_tasks", {
|
|
2386
|
+
status: "claimed"
|
|
2387
|
+
});
|
|
2388
|
+
assertDoctorMcpTask(claimedTask.task, {
|
|
2389
|
+
taskId: doneTask.task.id,
|
|
2390
|
+
status: "claimed",
|
|
2391
|
+
title: "Doctor HTTP complete smoke",
|
|
2392
|
+
claimedBy: "doctor-http-smoke"
|
|
2393
|
+
});
|
|
2394
|
+
assertDoctorMcpTaskInList(claimedTasks.tasks, {
|
|
2395
|
+
taskId: doneTask.task.id,
|
|
2396
|
+
status: "claimed"
|
|
2397
|
+
});
|
|
2398
|
+
const completed = await callHttpMcpJsonTool(client, "bridge_complete_task", {
|
|
2399
|
+
task_id: doneTask.task.id,
|
|
2400
|
+
summary: "Completed by doctor HTTP MCP",
|
|
2401
|
+
commands: ["doctor http finalizer smoke"]
|
|
2402
|
+
});
|
|
2403
|
+
const blockedTask = await callHttpMcpJsonTool(client, "bridge_create_task", {
|
|
2404
|
+
title: "Doctor HTTP block smoke",
|
|
2405
|
+
prompt: "Block this task over HTTP MCP"
|
|
2406
|
+
});
|
|
2407
|
+
const blocked = await callHttpMcpJsonTool(client, "bridge_block_task", {
|
|
2408
|
+
task_id: blockedTask.task.id,
|
|
2409
|
+
summary: "Blocked by doctor HTTP MCP",
|
|
2410
|
+
code: "doctor_http_blocker",
|
|
2411
|
+
retryable: true,
|
|
2412
|
+
next_step: "Inspect doctor output."
|
|
2413
|
+
});
|
|
2414
|
+
const fetchedDone = await callHttpMcpJsonTool(client, "bridge_fetch_result", {
|
|
2415
|
+
task_id: doneTask.task.id
|
|
2416
|
+
});
|
|
2417
|
+
const fetchedBlocked = await callHttpMcpJsonTool(client, "bridge_fetch_result", {
|
|
2418
|
+
task_id: blockedTask.task.id
|
|
2419
|
+
});
|
|
2420
|
+
const doneTasks = await callHttpMcpJsonTool(client, "bridge_list_tasks", {
|
|
2421
|
+
status: "done"
|
|
2422
|
+
});
|
|
2423
|
+
const blockedTasks = await callHttpMcpJsonTool(client, "bridge_list_tasks", {
|
|
2424
|
+
status: "blocked"
|
|
2425
|
+
});
|
|
2426
|
+
const results = await callHttpMcpJsonTool(client, "bridge_list_results", {});
|
|
2427
|
+
assertDoctorMcpResult(completed.result, {
|
|
2428
|
+
taskId: doneTask.task.id,
|
|
2429
|
+
status: "done",
|
|
2430
|
+
summary: "Completed by doctor HTTP MCP",
|
|
2431
|
+
commands: ["doctor http finalizer smoke"]
|
|
2432
|
+
});
|
|
2433
|
+
assertDoctorMcpResult(fetchedDone.result, {
|
|
2434
|
+
taskId: doneTask.task.id,
|
|
2435
|
+
status: "done",
|
|
2436
|
+
summary: "Completed by doctor HTTP MCP",
|
|
2437
|
+
commands: ["doctor http finalizer smoke"]
|
|
2438
|
+
});
|
|
2439
|
+
assertDoctorMcpResult(blocked.result, {
|
|
2440
|
+
taskId: blockedTask.task.id,
|
|
2441
|
+
status: "blocked",
|
|
2442
|
+
summary: "Blocked by doctor HTTP MCP",
|
|
2443
|
+
blockerCode: "doctor_http_blocker",
|
|
2444
|
+
retryable: true,
|
|
2445
|
+
nextStep: "Inspect doctor output."
|
|
2446
|
+
});
|
|
2447
|
+
assertDoctorMcpResult(fetchedBlocked.result, {
|
|
2448
|
+
taskId: blockedTask.task.id,
|
|
2449
|
+
status: "blocked",
|
|
2450
|
+
summary: "Blocked by doctor HTTP MCP",
|
|
2451
|
+
blockerCode: "doctor_http_blocker",
|
|
2452
|
+
retryable: true,
|
|
2453
|
+
nextStep: "Inspect doctor output."
|
|
2454
|
+
});
|
|
2455
|
+
assertDoctorMcpTaskInList(doneTasks.tasks, {
|
|
2456
|
+
taskId: doneTask.task.id,
|
|
2457
|
+
status: "done"
|
|
2458
|
+
});
|
|
2459
|
+
assertDoctorMcpTaskInList(blockedTasks.tasks, {
|
|
2460
|
+
taskId: blockedTask.task.id,
|
|
2461
|
+
status: "blocked"
|
|
2462
|
+
});
|
|
2463
|
+
assertDoctorMcpResultInList(results.results, {
|
|
2464
|
+
taskId: doneTask.task.id,
|
|
2465
|
+
status: "done",
|
|
2466
|
+
summary: "Completed by doctor HTTP MCP"
|
|
2467
|
+
});
|
|
2468
|
+
assertDoctorMcpResultInList(results.results, {
|
|
2469
|
+
taskId: blockedTask.task.id,
|
|
2470
|
+
status: "blocked",
|
|
2471
|
+
summary: "Blocked by doctor HTTP MCP"
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
async function callHttpMcpJsonTool(client, name, args) {
|
|
2475
|
+
const result = await withTimeout(client.callTool({ name, arguments: args }), 20_000, `timed out calling HTTP MCP tool ${name}`);
|
|
2476
|
+
const content = result.content;
|
|
2477
|
+
if (Array.isArray(content)) {
|
|
2478
|
+
for (const item of content) {
|
|
2479
|
+
if (isMcpTextContent(item))
|
|
2480
|
+
return JSON.parse(item.text);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
throw new Error(`HTTP MCP tool ${name} did not return text content`);
|
|
2484
|
+
}
|
|
2485
|
+
function isMcpTextContent(item) {
|
|
2486
|
+
return (typeof item === "object" &&
|
|
2487
|
+
item !== null &&
|
|
2488
|
+
"type" in item &&
|
|
2489
|
+
"text" in item &&
|
|
2490
|
+
item.type === "text" &&
|
|
2491
|
+
typeof item.text === "string");
|
|
2492
|
+
}
|
|
2493
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
2494
|
+
let timeout;
|
|
2495
|
+
try {
|
|
2496
|
+
return await Promise.race([
|
|
2497
|
+
promise,
|
|
2498
|
+
new Promise((_, reject) => {
|
|
2499
|
+
timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
2500
|
+
})
|
|
2501
|
+
]);
|
|
2502
|
+
}
|
|
2503
|
+
finally {
|
|
2504
|
+
if (timeout)
|
|
2505
|
+
clearTimeout(timeout);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
function assertDoctorMcpResult(result, expected) {
|
|
2509
|
+
if (result.task_id !== expected.taskId || result.status !== expected.status || result.summary !== expected.summary) {
|
|
2510
|
+
throw new Error(`unexpected HTTP MCP result: ${JSON.stringify(result)} expected ${JSON.stringify(expected)}`);
|
|
2511
|
+
}
|
|
2512
|
+
if (expected.commands && JSON.stringify(result.commands) !== JSON.stringify(expected.commands)) {
|
|
2513
|
+
throw new Error(`unexpected HTTP MCP result commands: ${JSON.stringify(result.commands)} expected ${JSON.stringify(expected.commands)}`);
|
|
2514
|
+
}
|
|
2515
|
+
if (expected.blockerCode && result.blocker?.code !== expected.blockerCode) {
|
|
2516
|
+
throw new Error(`unexpected HTTP MCP blocker: ${JSON.stringify(result.blocker)} expected code ${expected.blockerCode}`);
|
|
2517
|
+
}
|
|
2518
|
+
if (expected.retryable !== undefined && result.blocker?.retryable !== expected.retryable) {
|
|
2519
|
+
throw new Error(`unexpected HTTP MCP blocker retryable: ${JSON.stringify(result.blocker)} expected ${expected.retryable}`);
|
|
2520
|
+
}
|
|
2521
|
+
if (expected.nextStep !== undefined && result.blocker?.next_step !== expected.nextStep) {
|
|
2522
|
+
throw new Error(`unexpected HTTP MCP blocker next_step: ${JSON.stringify(result.blocker)} expected ${expected.nextStep}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function assertDoctorMcpTask(task, expected) {
|
|
2526
|
+
if (task.id !== expected.taskId || task.status !== expected.status) {
|
|
2527
|
+
throw new Error(`unexpected HTTP MCP task: ${JSON.stringify(task)} expected ${JSON.stringify(expected)}`);
|
|
2528
|
+
}
|
|
2529
|
+
if (expected.title !== undefined && task.title !== expected.title) {
|
|
2530
|
+
throw new Error(`unexpected HTTP MCP task title: ${JSON.stringify(task)} expected ${expected.title}`);
|
|
2531
|
+
}
|
|
2532
|
+
if (expected.claimedBy !== undefined && task.claimed_by !== expected.claimedBy) {
|
|
2533
|
+
throw new Error(`unexpected HTTP MCP task claimer: ${JSON.stringify(task)} expected ${expected.claimedBy}`);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
function assertDoctorMcpTaskInList(tasks, expected) {
|
|
2537
|
+
if (!Array.isArray(tasks)) {
|
|
2538
|
+
throw new Error(`unexpected HTTP MCP task list: ${JSON.stringify(tasks)}`);
|
|
2539
|
+
}
|
|
2540
|
+
if (!tasks.some((task) => isDoctorMcpTask(task) && task.id === expected.taskId && task.status === expected.status)) {
|
|
2541
|
+
throw new Error(`missing HTTP MCP task in list: ${JSON.stringify(expected)} from ${JSON.stringify(tasks)}`);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
function assertDoctorMcpResultInList(results, expected) {
|
|
2545
|
+
if (!Array.isArray(results)) {
|
|
2546
|
+
throw new Error(`unexpected HTTP MCP result list: ${JSON.stringify(results)}`);
|
|
2547
|
+
}
|
|
2548
|
+
if (!results.some((result) => isDoctorMcpResult(result) && result.task_id === expected.taskId && result.status === expected.status && result.summary === expected.summary)) {
|
|
2549
|
+
throw new Error(`missing HTTP MCP result in list: ${JSON.stringify(expected)} from ${JSON.stringify(results)}`);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function isDoctorMcpTask(task) {
|
|
2553
|
+
return typeof task === "object" && task !== null;
|
|
2554
|
+
}
|
|
2555
|
+
function isDoctorMcpResult(result) {
|
|
2556
|
+
return typeof result === "object" && result !== null;
|
|
2557
|
+
}
|
|
2558
|
+
async function runMcpWriteSmoke() {
|
|
2559
|
+
const cwd = await mkdtemp(path.join(tmpdir(), "prodex-doctor-"));
|
|
2560
|
+
let smokeFailed = false;
|
|
2561
|
+
try {
|
|
2562
|
+
await writeFile(path.join(cwd, "notes.md"), "old\n", "utf8");
|
|
2563
|
+
await execFileAsync("git", ["init"], { cwd });
|
|
2564
|
+
await execFileAsync("git", ["config", "user.email", "doctor@example.com"], { cwd });
|
|
2565
|
+
await execFileAsync("git", ["config", "user.name", "PROdex Doctor"], { cwd });
|
|
2566
|
+
await execFileAsync("git", ["add", "notes.md"], { cwd });
|
|
2567
|
+
await execFileAsync("git", ["commit", "-m", "initial"], { cwd });
|
|
2568
|
+
const { stdout: headOut } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd });
|
|
2569
|
+
const head = headOut.trim();
|
|
2570
|
+
const handlers = createMcpToolHandlers({ cwd });
|
|
2571
|
+
const dryRun = await handlers.repo_write_file_dry_run({
|
|
2572
|
+
path: "notes.md",
|
|
2573
|
+
content: "new\n",
|
|
2574
|
+
expected_head: head
|
|
2575
|
+
});
|
|
2576
|
+
const receipt = JSON.parse(await readFile(path.join(cwd, ".bridge", "receipts", `${dryRun.receipt.id}.json`), "utf8"));
|
|
2577
|
+
if (Object.hasOwn(receipt.metadata ?? {}, "new_content")) {
|
|
2578
|
+
throw new Error("dry-run receipt contains inline write payload");
|
|
2579
|
+
}
|
|
2580
|
+
if (typeof receipt.metadata?.new_content_artifact !== "string") {
|
|
2581
|
+
throw new Error("dry-run receipt is missing write payload artifact");
|
|
2582
|
+
}
|
|
2583
|
+
const applied = await handlers.repo_write_file_apply({
|
|
2584
|
+
receipt_id: dryRun.receipt.id,
|
|
2585
|
+
expected_head: head,
|
|
2586
|
+
preimage_sha256: dryRun.preimage_sha256
|
|
2587
|
+
});
|
|
2588
|
+
const staged = await handlers.repo_stage_reviewed_paths({
|
|
2589
|
+
receipt_ids: [applied.receipt.id],
|
|
2590
|
+
expected_head: head
|
|
2591
|
+
});
|
|
2592
|
+
const { stdout: stagedOut } = await execFileAsync("git", ["diff", "--cached", "--name-only"], { cwd });
|
|
2593
|
+
const stagedName = stagedOut.trim();
|
|
2594
|
+
if (stagedName !== "notes.md" || staged.paths.join(",") !== "notes.md") {
|
|
2595
|
+
throw new Error(`unexpected staged paths: ${stagedName || "<none>"}`);
|
|
2596
|
+
}
|
|
2597
|
+
return { path: "notes.md", receipt_payload: "artifact", staged: stagedName };
|
|
2598
|
+
}
|
|
2599
|
+
catch (error) {
|
|
2600
|
+
smokeFailed = true;
|
|
2601
|
+
throw error;
|
|
2602
|
+
}
|
|
2603
|
+
finally {
|
|
2604
|
+
try {
|
|
2605
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2606
|
+
}
|
|
2607
|
+
catch (error) {
|
|
2608
|
+
if (!smokeFailed)
|
|
2609
|
+
throw error;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
function printBrowserLoginGuide(stdout, input) {
|
|
2614
|
+
const loginCommand = formatBrowserLoginCommand(input.sourceCli, input.commandOptions);
|
|
2615
|
+
const runtimeCommandOptions = {
|
|
2616
|
+
...(input.commandOptions?.cwd ? { cwd: input.commandOptions.cwd } : {}),
|
|
2617
|
+
...(input.commandOptions?.port ? { port: input.commandOptions.port } : {})
|
|
2618
|
+
};
|
|
2619
|
+
const checkCommand = formatBrowserCheckCommand(input.sourceCli, runtimeCommandOptions);
|
|
2620
|
+
const smokeCommand = formatBrowserSmokeCommand(input.sourceCli, runtimeCommandOptions);
|
|
2621
|
+
stdout("ChatGPT Pro browser login");
|
|
2622
|
+
stdout(input.opened ? "Opened the dedicated Chrome window for ChatGPT." : "Dry run: no browser was opened.");
|
|
2623
|
+
stdout("");
|
|
2624
|
+
stdout("Steps:");
|
|
2625
|
+
if (input.opened) {
|
|
2626
|
+
stdout(`1. Log in manually at ${input.loginUrl} in the dedicated Chrome window.`);
|
|
2627
|
+
stdout("2. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2628
|
+
stdout("3. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
|
2629
|
+
stdout("4. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
|
|
2630
|
+
stdout("5. Select the Pro/Thinking model you want in the ChatGPT UI.");
|
|
2631
|
+
stdout(`6. Run \`${checkCommand}\` to confirm the session is reachable.`);
|
|
2632
|
+
stdout(`7. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2633
|
+
}
|
|
2634
|
+
else {
|
|
2635
|
+
stdout(`1. Run \`${loginCommand}\` without \`--dry-run\` to open the dedicated Chrome window.`);
|
|
2636
|
+
stdout(`2. Log in manually at ${input.loginUrl} in that Chrome window.`);
|
|
2637
|
+
stdout("3. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2638
|
+
stdout("4. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
|
2639
|
+
stdout("5. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
|
|
2640
|
+
stdout("6. Select the Pro/Thinking model you want in the ChatGPT UI.");
|
|
2641
|
+
stdout(`7. Run \`${checkCommand}\` to confirm the session is reachable.`);
|
|
2642
|
+
stdout(`8. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2643
|
+
}
|
|
2644
|
+
stdout("");
|
|
2645
|
+
stdout(`Profile: ${input.profileDir}`);
|
|
2646
|
+
stdout(`Debug: http://127.0.0.1:${input.port}`);
|
|
2647
|
+
if (input.opened) {
|
|
2648
|
+
stdout("You can close this Chrome window after check/smoke or when you are done. The dedicated profile is reused next time.");
|
|
2649
|
+
}
|
|
2650
|
+
else {
|
|
2651
|
+
stdout("The dedicated profile path above will be reused by the real login command.");
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
function printProBrowserHelp(stdout, sourceCli) {
|
|
2655
|
+
const cli = formatCliCommand(sourceCli);
|
|
2656
|
+
const sourceCliOption = formatSourceCliOption(sourceCli);
|
|
2657
|
+
const loginUsage = sourceCli
|
|
2658
|
+
? `${cli} pro browser login${sourceCliOption} [--cwd /absolute/path/to/repo] [--dry-run] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000]`
|
|
2659
|
+
: "prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000]";
|
|
2660
|
+
const checkUsage = sourceCli
|
|
2661
|
+
? `${cli} pro browser check${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]`
|
|
2662
|
+
: "prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]";
|
|
2663
|
+
const smokeUsage = sourceCli
|
|
2664
|
+
? `${cli} pro browser smoke${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]`
|
|
2665
|
+
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
2666
|
+
const askUsage = sourceCli
|
|
2667
|
+
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--file path] "prompt"`
|
|
2668
|
+
: 'prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--file path] "prompt"';
|
|
2669
|
+
stdout(`${cli} pro browser
|
|
2670
|
+
|
|
2671
|
+
Commands:
|
|
2672
|
+
${loginUsage}
|
|
2673
|
+
${checkUsage}
|
|
2674
|
+
${smokeUsage}
|
|
2675
|
+
${askUsage}
|
|
2676
|
+
|
|
2677
|
+
Visible-browser sends require a manual browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers.
|
|
2678
|
+
Use \`${cli} pro ask\` for dry-run/manual previews.
|
|
2679
|
+
\`${cli} pro browser ask${sourceCliOption}\` always attempts an explicit visible-browser send.`);
|
|
2680
|
+
}
|
|
2681
|
+
async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
|
|
2682
|
+
const outcome = await waitForBrowserLaunchReady(opened, timeoutMs);
|
|
2683
|
+
if (outcome.reachable)
|
|
2684
|
+
return;
|
|
2685
|
+
if (outcome.earlyExit) {
|
|
2686
|
+
const detail = formatBrowserEarlyExit(outcome.earlyExit);
|
|
2687
|
+
throw new Error(`Chrome/Chromium exited before DevTools became reachable (${detail}). Check the visible browser environment, profile lock, display access, or PRODEX_CHROME, then retry.`);
|
|
2688
|
+
}
|
|
2689
|
+
throw new Error(`Chrome/Chromium did not expose a reachable DevTools endpoint after launch. Check the visible browser environment, profile lock, display access, or PRODEX_CHROME, then retry.`);
|
|
2690
|
+
}
|
|
2691
|
+
async function waitForBrowserLaunchReady(opened, timeoutMs = 5_000) {
|
|
2692
|
+
const deadline = Date.now() + timeoutMs;
|
|
2693
|
+
let earlyExit;
|
|
2694
|
+
while (Date.now() <= deadline) {
|
|
2695
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
2696
|
+
const status = await getChatGptBrowserStatus({ port: opened.port, timeoutMs: Math.min(250, remainingMs) });
|
|
2697
|
+
if (status.reachable)
|
|
2698
|
+
return { reachable: true };
|
|
2699
|
+
earlyExit ??= await opened.waitForEarlyExit(1);
|
|
2700
|
+
if (earlyExit && (earlyExit.code !== 0 || earlyExit.signal || earlyExit.error)) {
|
|
2701
|
+
return { reachable: false, earlyExit };
|
|
2702
|
+
}
|
|
2703
|
+
if (Date.now() >= deadline)
|
|
2704
|
+
break;
|
|
2705
|
+
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
2706
|
+
}
|
|
2707
|
+
return { reachable: false, ...(earlyExit ? { earlyExit } : {}) };
|
|
2708
|
+
}
|
|
2709
|
+
function formatBrowserEarlyExit(exit) {
|
|
2710
|
+
if (!exit)
|
|
2711
|
+
return "no exit details";
|
|
2712
|
+
return exit.error ?? `exit code ${exit.code ?? "null"}${exit.signal ? ` signal ${exit.signal}` : ""}`;
|
|
2713
|
+
}
|
|
2714
|
+
function sleep(ms) {
|
|
2715
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2716
|
+
}
|
|
2717
|
+
function formatBrowserModelHints(modelHints) {
|
|
2718
|
+
const modelish = /\b(?:ChatGPT|GPT(?:-[\w.]+)?|Pro|Plus|Team|Enterprise|Thinking|Extra High|Auto)\b/i;
|
|
2719
|
+
const hints = [...new Set(modelHints.map((hint) => hint.trim()).filter((hint) => modelish.test(hint)))]
|
|
2720
|
+
.map((hint) => (hint.length > 80 ? `${hint.slice(0, 77)}...` : hint))
|
|
2721
|
+
.slice(0, 6);
|
|
2722
|
+
return hints.length > 0 ? hints.join(" | ") : undefined;
|
|
2723
|
+
}
|
|
2724
|
+
function browserReadinessNextStep(input) {
|
|
2725
|
+
if (!input.loggedInLikely) {
|
|
2726
|
+
return "Log in manually in the visible ChatGPT browser, then retry.";
|
|
2727
|
+
}
|
|
2728
|
+
if (!input.hasComposer) {
|
|
2729
|
+
return "Open a normal ChatGPT chat or Project thread, select the Pro/Thinking model, and retry.";
|
|
2730
|
+
}
|
|
2731
|
+
return "Review the visible ChatGPT browser state, then retry.";
|
|
2732
|
+
}
|
|
2733
|
+
async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
2734
|
+
const sourceCli = resolveOptionalFileFlag(io.cwd, args, "--source-cli");
|
|
2735
|
+
const setupHintCwd = readFlag(args, "--cwd") ? configCwd : undefined;
|
|
2736
|
+
io.stdout("prodex product check");
|
|
2737
|
+
let bridgeReady = false;
|
|
2738
|
+
try {
|
|
2739
|
+
bridgeReady = await store.hasReadyBridgeStorageReadOnly();
|
|
2740
|
+
io.stdout(bridgeReady
|
|
2741
|
+
? "bridge: ok (.bridge)"
|
|
2742
|
+
: `bridge: missing (.bridge) - run \`${formatInitCommand(sourceCli, { cwd: setupHintCwd })}\` when you need local task/result storage`);
|
|
2743
|
+
}
|
|
2744
|
+
catch (error) {
|
|
2745
|
+
io.stdout(`bridge: blocked - ${errorMessage(error)}`);
|
|
2746
|
+
}
|
|
2747
|
+
let configReady = false;
|
|
2748
|
+
try {
|
|
2749
|
+
const config = await loadLocalConfig(configCwd);
|
|
2750
|
+
const tokenStatus = getTokenExpiryStatus(config);
|
|
2751
|
+
if (tokenStatus.status === "expired") {
|
|
2752
|
+
io.stdout(`config: expired - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2753
|
+
}
|
|
2754
|
+
else {
|
|
2755
|
+
io.stdout(`config: ok ${redactServerUrl(config.server_url)} token_status=${tokenStatus.status}`);
|
|
2756
|
+
const warningLine = formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd);
|
|
2757
|
+
if (warningLine)
|
|
2758
|
+
io.stdout(warningLine);
|
|
2759
|
+
configReady = true;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
catch (error) {
|
|
2763
|
+
if (isMissingFileError(error)) {
|
|
2764
|
+
io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2765
|
+
}
|
|
2766
|
+
else {
|
|
2767
|
+
io.stdout(`config: failed ${sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd })}`);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
const browserStatus = await getChatGptBrowserStatus({
|
|
2771
|
+
port: readPortFlag(args, "--port") ?? 9333,
|
|
2772
|
+
timeoutMs: readPositiveNumberFlag(args, "--timeout-ms") ?? 1500
|
|
2773
|
+
});
|
|
2774
|
+
const browserCommandOptions = {
|
|
2775
|
+
cwd: setupHintCwd,
|
|
2776
|
+
port: readPortFlag(args, "--port") ?? undefined
|
|
2777
|
+
};
|
|
2778
|
+
let chatgptReady = false;
|
|
2779
|
+
const visibilityBlocker = chatGptVisibilityBlocker(browserStatus.visibilityState, browserStatus.url);
|
|
2780
|
+
if (!browserStatus.reachable) {
|
|
2781
|
+
io.stdout(`chatgpt: ${browserStatus.blocker?.code ?? "unreachable"} - ${browserStatus.blocker?.message ?? "browser is not reachable"}`);
|
|
2782
|
+
const nextStep = productCheckBrowserNextStep(browserStatus.blocker?.next_step, sourceCli, browserCommandOptions);
|
|
2783
|
+
if (nextStep)
|
|
2784
|
+
io.stdout(`next: ${nextStep}`);
|
|
2785
|
+
}
|
|
2786
|
+
else if (browserStatus.blocker) {
|
|
2787
|
+
const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
|
|
2788
|
+
io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
|
|
2789
|
+
const nextStep = productCheckBrowserNextStep(browserStatus.blocker.next_step, sourceCli, browserCommandOptions);
|
|
2790
|
+
if (nextStep)
|
|
2791
|
+
io.stdout(`next: ${nextStep}`);
|
|
2792
|
+
}
|
|
2793
|
+
else if (visibilityBlocker) {
|
|
2794
|
+
io.stdout(`chatgpt: blocked ${visibilityBlocker.code} visibility=${browserStatus.visibilityState ?? "unknown"} - ${visibilityBlocker.message}`);
|
|
2795
|
+
const nextStep = productCheckBrowserNextStep(visibilityBlocker.next_step, sourceCli, browserCommandOptions);
|
|
2796
|
+
if (nextStep)
|
|
2797
|
+
io.stdout(`next: ${nextStep}`);
|
|
2798
|
+
}
|
|
2799
|
+
else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
|
|
2800
|
+
io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
|
|
2801
|
+
chatgptReady = true;
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
|
|
2805
|
+
const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
|
|
2806
|
+
io.stdout(`next: ${nextStep}`);
|
|
2807
|
+
}
|
|
2808
|
+
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
2809
|
+
if (modelHints)
|
|
2810
|
+
io.stdout(`model_hints: ${modelHints}`);
|
|
2811
|
+
if (bridgeReady) {
|
|
2812
|
+
try {
|
|
2813
|
+
const latest = await latestTrustedConsult(store, { readOnly: false });
|
|
2814
|
+
if (latest) {
|
|
2815
|
+
for (const line of formatProductCheckLatestProLines(latest, sourceCli, browserCommandOptions))
|
|
2816
|
+
io.stdout(line);
|
|
2817
|
+
}
|
|
2818
|
+
else {
|
|
2819
|
+
io.stdout("latest_pro: missing");
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
catch (error) {
|
|
2823
|
+
if (isUntrustedResultError(error)) {
|
|
2824
|
+
io.stdout(`latest_pro: untrusted ${error.taskId} ${sourceAwareResultMessage(errorMessage(error), sourceCli, browserCommandOptions)}`);
|
|
2825
|
+
}
|
|
2826
|
+
else {
|
|
2827
|
+
io.stdout(`latest_pro: unavailable ${firstLine(errorMessage(error))}`);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
else {
|
|
2832
|
+
io.stdout("latest_pro: missing");
|
|
2833
|
+
}
|
|
2834
|
+
return bridgeReady && configReady && chatgptReady;
|
|
2835
|
+
}
|
|
2836
|
+
async function listTasksForInspection(store, status) {
|
|
2837
|
+
return store.listTasksReadOnly(status);
|
|
2838
|
+
}
|
|
2839
|
+
async function listResultsForInspection(store) {
|
|
2840
|
+
return store.listFinalizedResultsReadOnly();
|
|
2841
|
+
}
|
|
2842
|
+
async function listRawResultsForInspection(store) {
|
|
2843
|
+
return store.listResultsReadOnly();
|
|
2844
|
+
}
|
|
2845
|
+
async function listReceiptsForInspection(store, input = {}) {
|
|
2846
|
+
return store.listReceiptsReadOnly(input);
|
|
2847
|
+
}
|
|
2848
|
+
async function listSessionsForInspection(store, status) {
|
|
2849
|
+
return store.listSessionsReadOnly(status);
|
|
2850
|
+
}
|
|
2851
|
+
async function listConsults(store, options = {}) {
|
|
2852
|
+
const [tasks, results] = options.readOnly
|
|
2853
|
+
? await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)])
|
|
2854
|
+
: await Promise.all([store.listTasks(), store.listResults()]);
|
|
2855
|
+
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
2856
|
+
assertNoMissingTerminalConsultResults(tasks, results);
|
|
2857
|
+
assertNoOrphanConsultResults(tasksById, results);
|
|
2858
|
+
const records = results
|
|
2859
|
+
.map((result) => {
|
|
2860
|
+
const task = tasksById.get(result.task_id);
|
|
2861
|
+
return task ? { task, result } : undefined;
|
|
2862
|
+
})
|
|
2863
|
+
.filter((record) => Boolean(record && isConsultRecord(record)))
|
|
2864
|
+
.sort((a, b) => b.result.created_at.localeCompare(a.result.created_at));
|
|
2865
|
+
const finalized = [];
|
|
2866
|
+
for (const record of records) {
|
|
2867
|
+
finalized.push({ ...record, result: await store.getFinalizedResultReadOnly(record.result.task_id) });
|
|
2868
|
+
}
|
|
2869
|
+
return finalized;
|
|
2870
|
+
}
|
|
2871
|
+
async function listConsultListEntries(store, options = { readOnly: true }) {
|
|
2872
|
+
const [tasks, results] = options.readOnly === false
|
|
2873
|
+
? await Promise.all([store.listTasks(), store.listResults()])
|
|
2874
|
+
: await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)]);
|
|
2875
|
+
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
2876
|
+
assertNoMissingTerminalConsultResults(tasks, results);
|
|
2877
|
+
assertNoOrphanConsultResults(tasksById, results);
|
|
2878
|
+
const records = results
|
|
2879
|
+
.map((result) => {
|
|
2880
|
+
const task = tasksById.get(result.task_id);
|
|
2881
|
+
return task ? { task, result } : undefined;
|
|
2882
|
+
})
|
|
2883
|
+
.filter((record) => Boolean(record && isConsultRecord(record)))
|
|
2884
|
+
.sort((a, b) => b.result.created_at.localeCompare(a.result.created_at));
|
|
2885
|
+
const entries = [];
|
|
2886
|
+
for (const record of records) {
|
|
2887
|
+
try {
|
|
2888
|
+
entries.push({ kind: "trusted", consult: { ...record, result: await store.getFinalizedResultReadOnly(record.result.task_id) } });
|
|
2889
|
+
}
|
|
2890
|
+
catch (error) {
|
|
2891
|
+
if (isUntrustedResultError(error)) {
|
|
2892
|
+
entries.push({ kind: "untrusted", task: record.task, result: record.result, error });
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
throw error;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return entries;
|
|
2899
|
+
}
|
|
2900
|
+
async function latestTrustedConsult(store, options = { readOnly: true }) {
|
|
2901
|
+
const entries = await listConsultListEntries(store, options);
|
|
2902
|
+
const trusted = entries.find((entry) => entry.kind === "trusted");
|
|
2903
|
+
if (trusted)
|
|
2904
|
+
return trusted.consult;
|
|
2905
|
+
const untrusted = entries.find((entry) => entry.kind === "untrusted");
|
|
2906
|
+
if (untrusted)
|
|
2907
|
+
throw untrusted.error;
|
|
2908
|
+
return undefined;
|
|
2909
|
+
}
|
|
2910
|
+
function assertNoOrphanConsultResults(tasksById, results) {
|
|
2911
|
+
const orphan = results
|
|
2912
|
+
.filter((result) => !tasksById.has(result.task_id) && isConsultResult(result))
|
|
2913
|
+
.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.task_id.localeCompare(a.task_id))[0];
|
|
2914
|
+
if (orphan)
|
|
2915
|
+
throw orphanConsultResultError(orphan.task_id);
|
|
2916
|
+
}
|
|
2917
|
+
async function latestResultTaskId(store, options = {}) {
|
|
2918
|
+
const results = options.readOnly ? await listResultsForInspection(store) : await store.listResults();
|
|
2919
|
+
const result = results.at(-1);
|
|
2920
|
+
if (!result)
|
|
2921
|
+
throw new Error("No results found");
|
|
2922
|
+
return result.task_id;
|
|
2923
|
+
}
|
|
2924
|
+
async function latestRawResultTaskId(store) {
|
|
2925
|
+
const result = (await store.listResults()).at(-1);
|
|
2926
|
+
if (!result)
|
|
2927
|
+
throw new Error("No results found");
|
|
2928
|
+
return result.task_id;
|
|
2929
|
+
}
|
|
2930
|
+
async function latestTask(store, options = {}) {
|
|
2931
|
+
const tasks = options.readOnly ? await listTasksForInspection(store) : await store.listTasks();
|
|
2932
|
+
return tasks.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id))[0];
|
|
2933
|
+
}
|
|
2934
|
+
async function writeTaskCompleteArtifacts(store, values) {
|
|
2935
|
+
const artifacts = [];
|
|
2936
|
+
for (const value of values) {
|
|
2937
|
+
const separator = value.indexOf("=");
|
|
2938
|
+
if (separator <= 0) {
|
|
2939
|
+
throw new Error("tasks complete --artifact requires path=text");
|
|
2940
|
+
}
|
|
2941
|
+
const artifactPath = value.slice(0, separator);
|
|
2942
|
+
const content = value.slice(separator + 1);
|
|
2943
|
+
if (!artifactPath.trim()) {
|
|
2944
|
+
throw new Error("tasks complete --artifact requires path=text");
|
|
2945
|
+
}
|
|
2946
|
+
const storedPath = await store.writeArtifactText(artifactPath, content);
|
|
2947
|
+
artifacts.push({ path: storedPath, role: "result" });
|
|
2948
|
+
}
|
|
2949
|
+
return artifacts;
|
|
2950
|
+
}
|
|
2951
|
+
async function getConsult(store, taskId, options = {}) {
|
|
2952
|
+
let task;
|
|
2953
|
+
try {
|
|
2954
|
+
task = options.readOnly ? await store.getTaskReadOnly(taskId) : await store.getTask(taskId);
|
|
2955
|
+
}
|
|
2956
|
+
catch (error) {
|
|
2957
|
+
if (isMissingFileError(error))
|
|
2958
|
+
return undefined;
|
|
2959
|
+
throw error;
|
|
2960
|
+
}
|
|
2961
|
+
if (!isConsultTask(task))
|
|
2962
|
+
return undefined;
|
|
2963
|
+
let result;
|
|
2964
|
+
try {
|
|
2965
|
+
result = await store.getFinalizedResultReadOnly(taskId);
|
|
2966
|
+
}
|
|
2967
|
+
catch (error) {
|
|
2968
|
+
if (isMissingFileError(error)) {
|
|
2969
|
+
if (isTerminalTask(task) && isConsultTask(task))
|
|
2970
|
+
throw missingConsultResultError(task);
|
|
2971
|
+
return undefined;
|
|
2972
|
+
}
|
|
2973
|
+
throw error;
|
|
2974
|
+
}
|
|
2975
|
+
if (!task || !result)
|
|
2976
|
+
return undefined;
|
|
2977
|
+
const record = { task, result };
|
|
2978
|
+
return isConsultRecord(record) ? record : undefined;
|
|
2979
|
+
}
|
|
2980
|
+
function assertNoMissingTerminalConsultResults(tasks, results) {
|
|
2981
|
+
const resultTaskIds = new Set(results.map((result) => result.task_id));
|
|
2982
|
+
const missing = tasks
|
|
2983
|
+
.filter((task) => isTerminalTask(task) && isConsultTask(task) && !resultTaskIds.has(task.id))
|
|
2984
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id.localeCompare(a.id))[0];
|
|
2985
|
+
if (missing)
|
|
2986
|
+
throw missingConsultResultError(missing);
|
|
2987
|
+
}
|
|
2988
|
+
function isTerminalTask(task) {
|
|
2989
|
+
return task.status === "done" || task.status === "blocked";
|
|
2990
|
+
}
|
|
2991
|
+
function isConsultTask(task) {
|
|
2992
|
+
return task.provenance.adapter === "chatgpt-control";
|
|
2993
|
+
}
|
|
2994
|
+
function isConsultResult(result) {
|
|
2995
|
+
return result.commands.some((command) => /chatgpt|gpt pro|visible ChatGPT/i.test(command));
|
|
2996
|
+
}
|
|
2997
|
+
function missingConsultResultError(task) {
|
|
2998
|
+
return new Error(`GPT Pro answer is corrupt: task ${task.id} is ${task.status} but .bridge/results/${task.id}.json is missing. Restore the result file, retry the completion path, or move the task record aside, then retry.`);
|
|
2999
|
+
}
|
|
3000
|
+
function orphanConsultResultError(taskId) {
|
|
3001
|
+
return new Error(`GPT Pro answer is corrupt: result .bridge/results/${taskId}.json exists but .bridge/tasks/${taskId}.json is missing. Restore the task file or move the orphan result record aside, then retry.`);
|
|
3002
|
+
}
|
|
3003
|
+
function isConsultRecord(record) {
|
|
3004
|
+
return isConsultTask(record.task);
|
|
3005
|
+
}
|
|
3006
|
+
function formatProAnswer(consult, sourceCli, options = {}) {
|
|
3007
|
+
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
3008
|
+
const summary = sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker);
|
|
3009
|
+
const lines = [
|
|
3010
|
+
`task_id: ${consult.task.id}`,
|
|
3011
|
+
`status: ${consult.result.status}`,
|
|
3012
|
+
consult.task.provenance.thread ? `thread: ${consult.task.provenance.thread}` : undefined,
|
|
3013
|
+
`created_at: ${consult.result.created_at}`,
|
|
3014
|
+
"",
|
|
3015
|
+
summary
|
|
3016
|
+
].filter((line) => line !== undefined);
|
|
3017
|
+
if (blocker) {
|
|
3018
|
+
lines.push("", "blocker:", `- code: ${blocker.code}`, `- retryable: ${blocker.retryable}`);
|
|
3019
|
+
if (blocker.next_step)
|
|
3020
|
+
lines.push(`- next_step: ${blocker.next_step}`);
|
|
3021
|
+
}
|
|
3022
|
+
if (consult.result.warnings.length > 0) {
|
|
3023
|
+
lines.push("", "warnings:");
|
|
3024
|
+
for (const warning of consult.result.warnings)
|
|
3025
|
+
lines.push(`- ${warning}`);
|
|
3026
|
+
}
|
|
3027
|
+
return lines.join("\n");
|
|
3028
|
+
}
|
|
3029
|
+
function formatProListSummary(consult, sourceCli, options = {}) {
|
|
3030
|
+
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
3031
|
+
return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
|
|
3032
|
+
}
|
|
3033
|
+
function formatProductCheckLatestProLines(consult, sourceCli, options = {}) {
|
|
3034
|
+
if (consult.result.status === "blocked") {
|
|
3035
|
+
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
3036
|
+
const code = blocker?.code ?? "unknown";
|
|
3037
|
+
const retryable = blocker?.retryable ?? false;
|
|
3038
|
+
const lines = [`latest_pro: blocked ${consult.task.id} code=${code} retryable=${retryable} ${consult.result.created_at}`];
|
|
3039
|
+
if (blocker?.next_step)
|
|
3040
|
+
lines.push(`latest_pro_next: ${blocker.next_step}`);
|
|
3041
|
+
return lines;
|
|
3042
|
+
}
|
|
3043
|
+
return [`latest_pro: ok ${consult.task.id} ${consult.result.status} ${consult.result.created_at}`];
|
|
3044
|
+
}
|
|
3045
|
+
function sourceAwareProAnswerBlocker(consult, sourceCli, options = {}) {
|
|
3046
|
+
if (!consult.result.blocker)
|
|
3047
|
+
return undefined;
|
|
3048
|
+
const browserAware = sourceAwareBrowserBlocker(consult.result.blocker, sourceCli, options);
|
|
3049
|
+
if ((!sourceCli && !options.cwd && !options.port) || !isSmokeConsultRecord(consult) || !browserAware.next_step)
|
|
3050
|
+
return browserAware;
|
|
3051
|
+
const nextStep = productCheckBrowserNextStep(browserAware.next_step, sourceCli, options);
|
|
3052
|
+
return nextStep === browserAware.next_step ? browserAware : { ...browserAware, next_step: nextStep };
|
|
3053
|
+
}
|
|
3054
|
+
function sourceAwareProAnswerSummary(summary, originalBlocker, displayedBlocker) {
|
|
3055
|
+
const originalNextStep = originalBlocker?.next_step;
|
|
3056
|
+
const displayedNextStep = displayedBlocker?.next_step;
|
|
3057
|
+
if (!originalNextStep || !displayedNextStep || originalNextStep === displayedNextStep)
|
|
3058
|
+
return summary;
|
|
3059
|
+
return summary.replaceAll(originalNextStep, displayedNextStep);
|
|
3060
|
+
}
|
|
3061
|
+
function isSmokeConsultRecord(consult) {
|
|
3062
|
+
return consult.task.title === "GPT Pro smoke" || consult.result.commands.includes("visible ChatGPT browser smoke");
|
|
3063
|
+
}
|
|
3064
|
+
function receiptInspectionListSuffix(receipt) {
|
|
3065
|
+
const status = receipt.metadata.integrity_status;
|
|
3066
|
+
if (typeof status === "object" &&
|
|
3067
|
+
status !== null &&
|
|
3068
|
+
"trusted" in status &&
|
|
3069
|
+
status.trusted === false) {
|
|
3070
|
+
return "\tintegrity=untrusted";
|
|
3071
|
+
}
|
|
3072
|
+
return "";
|
|
3073
|
+
}
|
|
3074
|
+
function formatSession(session) {
|
|
3075
|
+
return JSON.stringify({
|
|
3076
|
+
id: session.id,
|
|
3077
|
+
status: session.status,
|
|
3078
|
+
direction: session.direction,
|
|
3079
|
+
backend: session.backend,
|
|
3080
|
+
project: session.project,
|
|
3081
|
+
thread: session.thread,
|
|
3082
|
+
task_id: session.task_id,
|
|
3083
|
+
blocker: session.blocker,
|
|
3084
|
+
warnings: session.warnings,
|
|
3085
|
+
created_at: session.created_at,
|
|
3086
|
+
last_used_at: session.last_used_at
|
|
3087
|
+
}, null, 2);
|
|
3088
|
+
}
|
|
3089
|
+
function formatProConsultArtifact(consult) {
|
|
3090
|
+
const lines = [`# ChatGPT Pro Consult`, "", `Thread: ${consult.url}`, `Title: ${consult.title}`, ""];
|
|
3091
|
+
if (consult.modelHints.length > 0) {
|
|
3092
|
+
lines.push("Model hints:", ...consult.modelHints.map((hint) => `- ${hint}`), "");
|
|
3093
|
+
}
|
|
3094
|
+
if (consult.warnings.length > 0) {
|
|
3095
|
+
lines.push("Warnings:", ...consult.warnings.map((warning) => `- ${warning}`), "");
|
|
3096
|
+
}
|
|
3097
|
+
lines.push("## Answer", "", consult.answer.trim(), "");
|
|
3098
|
+
return lines.join("\n");
|
|
3099
|
+
}
|
|
3100
|
+
async function writeSessionBestEffort(store, input, io) {
|
|
3101
|
+
try {
|
|
3102
|
+
await store.writeSession(input);
|
|
3103
|
+
}
|
|
3104
|
+
catch (error) {
|
|
3105
|
+
io.stderr(`session_record_warning: ${errorMessage(error)}`);
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
async function writeSessionBeforeBrowserSend(store, input) {
|
|
3109
|
+
try {
|
|
3110
|
+
await store.writeSession(input);
|
|
3111
|
+
}
|
|
3112
|
+
catch (error) {
|
|
3113
|
+
throw new Error(`failed to record running consult session before browser send: ${errorMessage(error)}`);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
function firstLine(value) {
|
|
3117
|
+
return value.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
|
|
3118
|
+
}
|
|
3119
|
+
function errorMessage(error) {
|
|
3120
|
+
return error instanceof Error ? error.message : String(error);
|
|
3121
|
+
}
|
|
3122
|
+
function browserSendBlockerFromError(error) {
|
|
3123
|
+
const blocker = typeof error === "object" && error !== null && "blocker" in error ? error.blocker : undefined;
|
|
3124
|
+
if (typeof blocker === "object" &&
|
|
3125
|
+
blocker !== null &&
|
|
3126
|
+
"code" in blocker &&
|
|
3127
|
+
"message" in blocker &&
|
|
3128
|
+
"retryable" in blocker &&
|
|
3129
|
+
typeof blocker.code === "string" &&
|
|
3130
|
+
typeof blocker.message === "string" &&
|
|
3131
|
+
typeof blocker.retryable === "boolean") {
|
|
3132
|
+
return {
|
|
3133
|
+
code: blocker.code,
|
|
3134
|
+
message: blocker.message,
|
|
3135
|
+
retryable: blocker.retryable,
|
|
3136
|
+
...("next_step" in blocker && typeof blocker.next_step === "string" ? { next_step: blocker.next_step } : {})
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
const message = errorMessage(error);
|
|
3140
|
+
return {
|
|
3141
|
+
code: "browser_send_failed",
|
|
3142
|
+
message,
|
|
3143
|
+
retryable: true,
|
|
3144
|
+
next_step: "Resolve the visible browser issue manually, then rerun the consult if needed."
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
function isMissingFileError(error) {
|
|
3148
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3149
|
+
}
|
|
3150
|
+
function isUntrustedResultError(error) {
|
|
3151
|
+
return (typeof error === "object" &&
|
|
3152
|
+
error !== null &&
|
|
3153
|
+
"code" in error &&
|
|
3154
|
+
"taskId" in error &&
|
|
3155
|
+
error.code === "EUNTRUSTED_RESULT" &&
|
|
3156
|
+
typeof error.taskId === "string");
|
|
3157
|
+
}
|
|
3158
|
+
function assertTokenNotExpiredForCommand(config, sourceCli, setupHintCwd) {
|
|
3159
|
+
const tokenStatus = getTokenExpiryStatus(config);
|
|
3160
|
+
if (tokenStatus.status === "expired") {
|
|
3161
|
+
throw new Error(sourceAwareSetupMessage(tokenStatus.warning.toLowerCase(), sourceCli, { cwd: setupHintCwd }));
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
async function loadLocalConfigForCommand(cwd, command, sourceCli, setupHintCwd) {
|
|
3165
|
+
return loadLocalConfig(cwd).catch(async (error) => {
|
|
3166
|
+
if (isMissingFileError(error)) {
|
|
3167
|
+
throw new Error(sourceAwareSetupMessage(`${command} requires local MCP setup. Run \`prodex setup\` first. Add \`--token-ttl-hours <hours>\` before revealing token URLs, using tunnels, or connecting ChatGPT Projects.`, sourceCli, { cwd: setupHintCwd }));
|
|
3168
|
+
}
|
|
3169
|
+
throw new Error(sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd }));
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
function redactServerUrl(value) {
|
|
3173
|
+
return formatServerUrlForOutput(value, { showToken: false });
|
|
3174
|
+
}
|
|
3175
|
+
function formatServerUrlForOutput(value, options) {
|
|
3176
|
+
try {
|
|
3177
|
+
const url = new URL(value);
|
|
3178
|
+
url.username = "";
|
|
3179
|
+
url.password = "";
|
|
3180
|
+
if (!options.showToken && url.searchParams.has("prodex_token"))
|
|
3181
|
+
url.searchParams.set("prodex_token", "***");
|
|
3182
|
+
return url.toString();
|
|
3183
|
+
}
|
|
3184
|
+
catch {
|
|
3185
|
+
const withoutUserinfo = value.replace(/\/\/[^/@\s]+@/g, "//");
|
|
3186
|
+
return options.showToken ? withoutUserinfo : withoutUserinfo.replace(/([?&]prodex_token=)[^&]+/g, "$1***");
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function makeTunnelMcpUrl(publicUrl, token) {
|
|
3190
|
+
const url = parseTunnelPublicUrl(publicUrl);
|
|
3191
|
+
url.username = "";
|
|
3192
|
+
url.password = "";
|
|
3193
|
+
url.pathname = "/mcp";
|
|
3194
|
+
url.search = "";
|
|
3195
|
+
url.hash = "";
|
|
3196
|
+
url.searchParams.set("prodex_token", token);
|
|
3197
|
+
return url.toString();
|
|
3198
|
+
}
|
|
3199
|
+
function parseTunnelPublicUrl(publicUrl) {
|
|
3200
|
+
let url;
|
|
3201
|
+
try {
|
|
3202
|
+
url = new URL(publicUrl);
|
|
3203
|
+
}
|
|
3204
|
+
catch {
|
|
3205
|
+
throw new Error("--public-url must be a valid URL");
|
|
3206
|
+
}
|
|
3207
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
3208
|
+
throw new Error("--public-url must use http or https");
|
|
3209
|
+
}
|
|
3210
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
3211
|
+
throw new Error("--public-url must use https for non-loopback tunnel URLs");
|
|
3212
|
+
}
|
|
3213
|
+
return url;
|
|
3214
|
+
}
|
|
3215
|
+
function isLoopbackHost(hostname) {
|
|
3216
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
3217
|
+
}
|
|
3218
|
+
function readFlag(args, flag) {
|
|
3219
|
+
const index = args.indexOf(flag);
|
|
3220
|
+
if (index === -1)
|
|
3221
|
+
return undefined;
|
|
3222
|
+
return readFlagValue(args, index, flag);
|
|
3223
|
+
}
|
|
3224
|
+
function readNumberFlag(args, flag) {
|
|
3225
|
+
const raw = readFlag(args, flag);
|
|
3226
|
+
if (raw === undefined)
|
|
3227
|
+
return undefined;
|
|
3228
|
+
const value = Number(raw);
|
|
3229
|
+
if (!Number.isFinite(value))
|
|
3230
|
+
throw new Error(`${flag} requires a finite number`);
|
|
3231
|
+
return value;
|
|
3232
|
+
}
|
|
3233
|
+
function readPositiveNumberFlag(args, flag) {
|
|
3234
|
+
const value = readNumberFlag(args, flag);
|
|
3235
|
+
if (value === undefined)
|
|
3236
|
+
return undefined;
|
|
3237
|
+
if (value <= 0)
|
|
3238
|
+
throw new Error(`${flag} must be greater than 0`);
|
|
3239
|
+
return value;
|
|
3240
|
+
}
|
|
3241
|
+
function readPortFlag(args, flag) {
|
|
3242
|
+
const value = readNumberFlag(args, flag);
|
|
3243
|
+
if (value === undefined)
|
|
3244
|
+
return undefined;
|
|
3245
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
3246
|
+
throw new Error(`${flag} must be an integer from 1 to 65535`);
|
|
3247
|
+
}
|
|
3248
|
+
return value;
|
|
3249
|
+
}
|
|
3250
|
+
function readChatGptBrowserUrlFlag(args) {
|
|
3251
|
+
return normalizeChatGptTargetUrl(readFlag(args, "--url") ?? "https://chatgpt.com/");
|
|
3252
|
+
}
|
|
3253
|
+
function readRepeatedFlag(args, flag) {
|
|
3254
|
+
const values = [];
|
|
3255
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3256
|
+
if (args[index] === flag) {
|
|
3257
|
+
values.push(readFlagValue(args, index, flag));
|
|
3258
|
+
index += 1;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return values;
|
|
3262
|
+
}
|
|
3263
|
+
function resolveCwdFlag(defaultCwd, args) {
|
|
3264
|
+
const cwd = readFlag(args, "--cwd");
|
|
3265
|
+
if (!cwd)
|
|
3266
|
+
return defaultCwd;
|
|
3267
|
+
return resolveExistingDirectoryFlag(defaultCwd, cwd, "--cwd");
|
|
3268
|
+
}
|
|
3269
|
+
function resolveOptionalPathFlag(defaultCwd, args, flag) {
|
|
3270
|
+
const value = readFlag(args, flag);
|
|
3271
|
+
return value ? resolveExistingPathFlag(defaultCwd, value, flag) : undefined;
|
|
3272
|
+
}
|
|
3273
|
+
function resolveOptionalFileFlag(defaultCwd, args, flag) {
|
|
3274
|
+
const value = readFlag(args, flag);
|
|
3275
|
+
return value ? resolveExistingFileFlag(defaultCwd, value, flag) : undefined;
|
|
3276
|
+
}
|
|
3277
|
+
function resolveExistingPathFlag(defaultCwd, value, flag) {
|
|
3278
|
+
const resolved = path.resolve(defaultCwd, value);
|
|
3279
|
+
try {
|
|
3280
|
+
return realpathSync(resolved);
|
|
3281
|
+
}
|
|
3282
|
+
catch {
|
|
3283
|
+
throw new Error(`${flag} does not exist or is not accessible: ${resolved}`);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
function resolveExistingFileFlag(defaultCwd, value, flag) {
|
|
3287
|
+
const resolved = resolveExistingPathFlag(defaultCwd, value, flag);
|
|
3288
|
+
if (!statSync(resolved).isFile()) {
|
|
3289
|
+
throw new Error(`${flag} must be a file: ${resolved}`);
|
|
3290
|
+
}
|
|
3291
|
+
return resolved;
|
|
3292
|
+
}
|
|
3293
|
+
function resolveExistingDirectoryFlag(defaultCwd, value, flag) {
|
|
3294
|
+
const resolved = resolveExistingPathFlag(defaultCwd, value, flag);
|
|
3295
|
+
if (!statSync(resolved).isDirectory()) {
|
|
3296
|
+
throw new Error(`${flag} must be a directory: ${resolved}`);
|
|
3297
|
+
}
|
|
3298
|
+
return resolved;
|
|
3299
|
+
}
|
|
3300
|
+
function assertOnlyOptions(args, command, valueFlags, booleanFlags = []) {
|
|
3301
|
+
const valueFlagSet = new Set(valueFlags);
|
|
3302
|
+
const booleanFlagSet = new Set(booleanFlags);
|
|
3303
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3304
|
+
const arg = args[index];
|
|
3305
|
+
if (valueFlagSet.has(arg)) {
|
|
3306
|
+
readFlagValue(args, index, arg);
|
|
3307
|
+
index += 1;
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3310
|
+
if (booleanFlagSet.has(arg))
|
|
3311
|
+
continue;
|
|
3312
|
+
if (arg.startsWith("-")) {
|
|
3313
|
+
throw unknownOptionError(arg, command, [...valueFlagSet, ...booleanFlagSet]);
|
|
3314
|
+
}
|
|
3315
|
+
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
function readPositionalsWithOptions(args, command, maxPositionals, valueFlags, booleanFlags = []) {
|
|
3319
|
+
const valueFlagSet = new Set(valueFlags);
|
|
3320
|
+
const booleanFlagSet = new Set(booleanFlags);
|
|
3321
|
+
const positionals = [];
|
|
3322
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3323
|
+
const arg = args[index];
|
|
3324
|
+
if (valueFlagSet.has(arg)) {
|
|
3325
|
+
readFlagValue(args, index, arg);
|
|
3326
|
+
index += 1;
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
if (booleanFlagSet.has(arg))
|
|
3330
|
+
continue;
|
|
3331
|
+
if (arg.startsWith("-")) {
|
|
3332
|
+
throw unknownOptionError(arg, command, [...valueFlagSet, ...booleanFlagSet]);
|
|
3333
|
+
}
|
|
3334
|
+
if (positionals.length >= maxPositionals) {
|
|
3335
|
+
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
3336
|
+
}
|
|
3337
|
+
positionals.push(arg);
|
|
3338
|
+
}
|
|
3339
|
+
return positionals;
|
|
3340
|
+
}
|
|
3341
|
+
function assertNoExtraArgs(args, command, maxPositionals) {
|
|
3342
|
+
for (const arg of args.slice(maxPositionals)) {
|
|
3343
|
+
if (arg.startsWith("-")) {
|
|
3344
|
+
throw new Error(`Unknown option for ${command}: ${arg}`);
|
|
3345
|
+
}
|
|
3346
|
+
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
function readRequiredLeadingArgument(args, command, placeholder) {
|
|
3350
|
+
const value = args[0];
|
|
3351
|
+
if (!value || value.startsWith("-"))
|
|
3352
|
+
throw new Error(`${command} requires ${placeholder}`);
|
|
3353
|
+
return value;
|
|
3354
|
+
}
|
|
3355
|
+
const ASK_PRO_BOOLEAN_FLAGS = new Set(["--dry-run", "--send", "--confirm-target"]);
|
|
3356
|
+
const ASK_PRO_VALUE_FLAGS = new Set(["--cwd", "--file", "--port", "--timeout-ms", "--target-url", "--source-cli"]);
|
|
3357
|
+
const ASK_PRO_PREVIEW_VALUE_FLAGS = new Set(["--cwd", "--file", "--port", "--timeout-ms", "--target-url"]);
|
|
3358
|
+
function parseAskProArgs(args, valueFlags = ASK_PRO_VALUE_FLAGS) {
|
|
3359
|
+
const delimiterIndex = args.indexOf("--");
|
|
3360
|
+
const optionArgs = delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
3361
|
+
const promptTail = delimiterIndex === -1 ? [] : args.slice(delimiterIndex + 1);
|
|
3362
|
+
const positionalPromptParts = [];
|
|
3363
|
+
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
3364
|
+
const arg = optionArgs[index];
|
|
3365
|
+
if (!arg.startsWith("--")) {
|
|
3366
|
+
if (arg.startsWith("-"))
|
|
3367
|
+
throw unknownOptionError(arg, undefined, [...valueFlags, ...ASK_PRO_BOOLEAN_FLAGS]);
|
|
3368
|
+
positionalPromptParts.push(arg);
|
|
3369
|
+
continue;
|
|
3370
|
+
}
|
|
3371
|
+
if (ASK_PRO_BOOLEAN_FLAGS.has(arg))
|
|
3372
|
+
continue;
|
|
3373
|
+
if (valueFlags.has(arg)) {
|
|
3374
|
+
readFlagValue(optionArgs, index, arg);
|
|
3375
|
+
index += 1;
|
|
3376
|
+
continue;
|
|
3377
|
+
}
|
|
3378
|
+
throw unknownOptionError(arg, undefined, [...valueFlags, ...ASK_PRO_BOOLEAN_FLAGS]);
|
|
3379
|
+
}
|
|
3380
|
+
return { optionArgs, promptParts: [...positionalPromptParts, ...promptTail] };
|
|
3381
|
+
}
|
|
3382
|
+
function askProOptionArgs(args) {
|
|
3383
|
+
const delimiterIndex = args.indexOf("--");
|
|
3384
|
+
return delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
3385
|
+
}
|
|
3386
|
+
function hasAskProMode(args) {
|
|
3387
|
+
const optionArgs = askProOptionArgs(args);
|
|
3388
|
+
return optionArgs.includes("--send") || optionArgs.includes("--dry-run");
|
|
3389
|
+
}
|
|
3390
|
+
function hasAskProSendMode(args) {
|
|
3391
|
+
return askProOptionArgs(args).includes("--send");
|
|
3392
|
+
}
|
|
3393
|
+
function hasAskProDryRunMode(args) {
|
|
3394
|
+
return askProOptionArgs(args).includes("--dry-run");
|
|
3395
|
+
}
|
|
3396
|
+
function readFlagValue(args, index, flag) {
|
|
3397
|
+
const value = args[index + 1];
|
|
3398
|
+
if (!value || value.startsWith("--"))
|
|
3399
|
+
throw new Error(`${flag} requires a value`);
|
|
3400
|
+
return value;
|
|
3401
|
+
}
|
|
3402
|
+
function readSessionStatusFlag(args) {
|
|
3403
|
+
const value = readFlag(args, "--status");
|
|
3404
|
+
if (value === undefined)
|
|
3405
|
+
return undefined;
|
|
3406
|
+
if (value === "preview" || value === "running" || value === "done" || value === "blocked")
|
|
3407
|
+
return value;
|
|
3408
|
+
throw new Error("--status must be one of preview, running, done, blocked");
|
|
3409
|
+
}
|
|
3410
|
+
const TASK_STATUSES = TaskStatusSchema.options;
|
|
3411
|
+
function readTaskStatusFlag(args) {
|
|
3412
|
+
const value = readFlag(args, "--status");
|
|
3413
|
+
if (value === undefined)
|
|
3414
|
+
return undefined;
|
|
3415
|
+
if (TaskStatusSchema.safeParse(value).success)
|
|
3416
|
+
return value;
|
|
3417
|
+
throw new Error(`--status must be one of ${TASK_STATUSES.join(", ")}`);
|
|
3418
|
+
}
|
|
3419
|
+
const RECEIPT_KINDS = ReceiptKindSchema.options;
|
|
3420
|
+
function readReceiptKindFlag(args) {
|
|
3421
|
+
const value = readFlag(args, "--kind");
|
|
3422
|
+
if (value === undefined)
|
|
3423
|
+
return undefined;
|
|
3424
|
+
if (ReceiptKindSchema.safeParse(value).success)
|
|
3425
|
+
return value;
|
|
3426
|
+
throw new Error(`--kind must be one of ${RECEIPT_KINDS.join(", ")}`);
|
|
3427
|
+
}
|
|
3428
|
+
function formatTokenExpiryLine(config) {
|
|
3429
|
+
const tokenStatus = getTokenExpiryStatus(config);
|
|
3430
|
+
if (tokenStatus.status === "valid")
|
|
3431
|
+
return `Token expires: ${tokenStatus.token_expires_at}`;
|
|
3432
|
+
if (tokenStatus.status === "expired")
|
|
3433
|
+
return `Token expired: ${tokenStatus.token_expires_at}`;
|
|
3434
|
+
return "Token expires: never (local-only; use --token-ttl-hours before exposing through a tunnel).";
|
|
3435
|
+
}
|
|
3436
|
+
function formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd) {
|
|
3437
|
+
return tokenStatus.warning ? `config_warning: ${sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })}` : undefined;
|
|
3438
|
+
}
|
|
3439
|
+
async function ensureBridgeGitignore(cwd) {
|
|
3440
|
+
const bridgeIgnorePath = path.join(cwd, ".bridge", ".gitignore");
|
|
3441
|
+
await mkdir(path.dirname(bridgeIgnorePath), { recursive: true });
|
|
3442
|
+
await writeVerifiedUtf8File(bridgeIgnorePath, ["tasks/*.json", "results/*.json", "sessions/*.json", "receipts/*.json", "artifacts/*", "config.local.json", "receipt-key.local", "!.gitignore", ""].join("\n"), () => assertGitignoreTargetSafe(bridgeIgnorePath), { create: true });
|
|
3443
|
+
const rootIgnorePath = path.join(cwd, ".gitignore");
|
|
3444
|
+
let current = "";
|
|
3445
|
+
try {
|
|
3446
|
+
current = await readVerifiedUtf8File(rootIgnorePath, () => assertGitignoreTargetSafe(rootIgnorePath));
|
|
3447
|
+
}
|
|
3448
|
+
catch (error) {
|
|
3449
|
+
if (!isMissingFileError(error))
|
|
3450
|
+
throw error;
|
|
3451
|
+
}
|
|
3452
|
+
const ignored = new Set(current.split(/\r?\n/).filter(Boolean));
|
|
3453
|
+
const additions = ["node_modules/", "dist/"].filter((line) => !ignored.has(line));
|
|
3454
|
+
if (additions.length > 0) {
|
|
3455
|
+
await writeVerifiedUtf8File(rootIgnorePath, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${additions.join("\n")}\n`, () => assertGitignoreTargetSafe(rootIgnorePath), { create: true });
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
async function assertGitignoreTargetSafe(filePath) {
|
|
3459
|
+
try {
|
|
3460
|
+
const stat = await lstat(filePath);
|
|
3461
|
+
if (stat.isSymbolicLink())
|
|
3462
|
+
throw new Error(`${filePath} must not be a symlink`);
|
|
3463
|
+
if (!stat.isFile())
|
|
3464
|
+
throw new Error(`${filePath} must be a regular file`);
|
|
3465
|
+
}
|
|
3466
|
+
catch (error) {
|
|
3467
|
+
if (isMissingFileError(error))
|
|
3468
|
+
return;
|
|
3469
|
+
throw error;
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
async function waitForShutdown(close) {
|
|
3473
|
+
await new Promise((resolve) => {
|
|
3474
|
+
const shutdown = () => resolve();
|
|
3475
|
+
process.once("SIGINT", shutdown);
|
|
3476
|
+
process.once("SIGTERM", shutdown);
|
|
3477
|
+
});
|
|
3478
|
+
await close();
|
|
3479
|
+
}
|
|
3480
|
+
function isDirectCliInvocation() {
|
|
3481
|
+
if (!process.argv[1])
|
|
3482
|
+
return false;
|
|
3483
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
3484
|
+
try {
|
|
3485
|
+
return realpathSync(process.argv[1]) === realpathSync(modulePath);
|
|
3486
|
+
}
|
|
3487
|
+
catch {
|
|
3488
|
+
return path.resolve(process.argv[1]) === modulePath;
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
if (isDirectCliInvocation()) {
|
|
3492
|
+
runCli(process.argv.slice(2))
|
|
3493
|
+
.then((code) => {
|
|
3494
|
+
if (code !== 0)
|
|
3495
|
+
process.exitCode = code;
|
|
3496
|
+
})
|
|
3497
|
+
.catch((error) => {
|
|
3498
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3499
|
+
process.exitCode = 1;
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
//# sourceMappingURL=cli.js.map
|