@delexec/ops 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller/README.md +3 -0
- package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller/package.json +53 -0
- package/node_modules/@delexec/caller-controller/src/server.js +127 -0
- package/node_modules/@delexec/caller-controller-core/README.md +3 -0
- package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller-core/package.json +26 -0
- package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
- package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
- package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
- package/node_modules/@delexec/responder-controller/README.md +3 -0
- package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
- package/node_modules/@delexec/responder-controller/package.json +53 -0
- package/node_modules/@delexec/responder-controller/src/server.js +254 -0
- package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
- package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
- package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
- package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
- package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
- package/node_modules/@delexec/runtime-utils/README.md +3 -0
- package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
- package/node_modules/@delexec/runtime-utils/package.json +23 -0
- package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
- package/node_modules/@delexec/sqlite-store/README.md +3 -0
- package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
- package/node_modules/@delexec/sqlite-store/package.json +26 -0
- package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
- package/node_modules/@delexec/transport-email/README.md +3 -0
- package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-email/package.json +23 -0
- package/node_modules/@delexec/transport-email/src/index.js +185 -0
- package/node_modules/@delexec/transport-emailengine/README.md +3 -0
- package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-emailengine/package.json +26 -0
- package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
- package/node_modules/@delexec/transport-gmail/README.md +3 -0
- package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-gmail/package.json +26 -0
- package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
- package/node_modules/@delexec/transport-relay-http/README.md +3 -0
- package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-relay-http/package.json +23 -0
- package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
- package/package.json +64 -0
- package/src/cli.js +1571 -0
- package/src/config.js +1180 -0
- package/src/example-hotline-worker.js +65 -0
- package/src/example-hotline.js +196 -0
- package/src/logging.js +56 -0
- package/src/supervisor.js +3070 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { ensureOpsDirectories, readJsonFile } from "@delexec/runtime-utils";
|
|
9
|
+
import { buildStructuredError } from "@delexec/contracts";
|
|
10
|
+
import { createOpsSupervisorServer } from "./supervisor.js";
|
|
11
|
+
import {
|
|
12
|
+
buildHotlineOnboardingBody,
|
|
13
|
+
ensureHotlineRegistrationDraft,
|
|
14
|
+
ensureOpsState,
|
|
15
|
+
ensureResponderIdentity,
|
|
16
|
+
loadHotlineRegistrationDraft,
|
|
17
|
+
removeHotline,
|
|
18
|
+
saveOpsState,
|
|
19
|
+
setHotlineEnabled,
|
|
20
|
+
upsertHotline
|
|
21
|
+
} from "./config.js";
|
|
22
|
+
import { buildExampleHotlineDefinition, isExampleHotlineDefinitionStale, LOCAL_EXAMPLE_HOTLINE_ID } from "./example-hotline.js";
|
|
23
|
+
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
const CLI_PATH = fileURLToPath(import.meta.url);
|
|
26
|
+
const CLIENT_ROOT = path.resolve(path.dirname(CLI_PATH), "../../..");
|
|
27
|
+
const OPS_CONSOLE_DIR = path.join(CLIENT_ROOT, "apps/ops-console");
|
|
28
|
+
const DEFAULT_CONSOLE_HOST = "127.0.0.1";
|
|
29
|
+
const DEFAULT_CONSOLE_PORT = 4173;
|
|
30
|
+
const DEFAULT_UI_READY_TIMEOUT_MS = 120000;
|
|
31
|
+
const OPS_SESSION_HEADER = "X-Ops-Session";
|
|
32
|
+
|
|
33
|
+
function getOpsSessionFile() {
|
|
34
|
+
return path.join(ensureOpsDirectories(), "run", "session.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function usage() {
|
|
38
|
+
console.log(`Usage:
|
|
39
|
+
delexec-ops setup
|
|
40
|
+
delexec-ops start
|
|
41
|
+
delexec-ops status
|
|
42
|
+
delexec-ops bootstrap [--email <email>] [--platform <url>] [--text <text>] [--open-ui] [--ui-port <port>] [--ui-host <host>] [--no-browser]
|
|
43
|
+
delexec-ops ui start [--host <host>] [--port <port>] [--open] [--no-browser]
|
|
44
|
+
delexec-ops mcp spec
|
|
45
|
+
delexec-ops auth register --email <email> [--local] [--platform <url>]
|
|
46
|
+
delexec-ops enable-responder [--responder-id <id>] [--display-name <name>]
|
|
47
|
+
delexec-ops add-hotline --type <process|http> --hotline-id <id> [--cmd <command> | --url <url>] [--cwd <path>] [--env KEY=VALUE]
|
|
48
|
+
delexec-ops attach-project --project-path <path> [--project-name <name>] [--project-description <text>] [--hotline-id <id>] [--cmd <command> | --url <url>] [--cwd <path>] [--env KEY=VALUE] [--task-type <type>] [--capability <capability>]
|
|
49
|
+
delexec-ops add-example-hotline
|
|
50
|
+
delexec-ops remove-hotline --hotline-id <id>
|
|
51
|
+
delexec-ops enable-hotline --hotline-id <id>
|
|
52
|
+
delexec-ops disable-hotline --hotline-id <id>
|
|
53
|
+
delexec-ops submit-review [--hotline-id <id>]
|
|
54
|
+
delexec-ops responder show-draft --hotline-id <id>
|
|
55
|
+
delexec-ops responder submit-draft --hotline-id <id>
|
|
56
|
+
delexec-ops run-example [--text <text>]
|
|
57
|
+
delexec-ops doctor
|
|
58
|
+
delexec-ops debug-snapshot
|
|
59
|
+
|
|
60
|
+
Product terms:
|
|
61
|
+
Caller = Caller
|
|
62
|
+
Responder = Responder
|
|
63
|
+
Hotline = catalog-facing service entry backed by a responder/hotline pair
|
|
64
|
+
Platform Control = web UI for operator review and oversight
|
|
65
|
+
|
|
66
|
+
Compatibility:
|
|
67
|
+
delexec-ops responder init
|
|
68
|
+
delexec-ops responder register
|
|
69
|
+
delexec-ops responder add-hotline ...
|
|
70
|
+
delexec-ops responder start
|
|
71
|
+
delexec-ops responder status
|
|
72
|
+
delexec-ops responder doctor
|
|
73
|
+
delexec-ops responder init
|
|
74
|
+
delexec-ops responder register
|
|
75
|
+
delexec-ops responder add-hotline ...
|
|
76
|
+
delexec-ops responder start
|
|
77
|
+
delexec-ops responder status
|
|
78
|
+
delexec-ops responder doctor`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseArgs(argv) {
|
|
82
|
+
const args = { _: [] };
|
|
83
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
84
|
+
const token = argv[index];
|
|
85
|
+
if (!token.startsWith("--")) {
|
|
86
|
+
args._.push(token);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const key = token.slice(2);
|
|
90
|
+
const next = argv[index + 1];
|
|
91
|
+
const value = !next || next.startsWith("--") ? true : next;
|
|
92
|
+
if (value !== true) {
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
if (args[key] === undefined) {
|
|
96
|
+
args[key] = value;
|
|
97
|
+
} else if (Array.isArray(args[key])) {
|
|
98
|
+
args[key].push(value);
|
|
99
|
+
} else {
|
|
100
|
+
args[key] = [args[key], value];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return args;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function emit(value) {
|
|
107
|
+
console.log(JSON.stringify(value, null, 2));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function logBootstrapStep(steps, step, ok, detail = {}) {
|
|
111
|
+
steps.push({ step, ok, ...detail });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getValues(value) {
|
|
115
|
+
if (value === undefined || value === null || value === false) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseEnvAssignments(value) {
|
|
122
|
+
const entries = getValues(value);
|
|
123
|
+
const env = {};
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const normalized = String(entry || "").trim();
|
|
126
|
+
const separatorIndex = normalized.indexOf("=");
|
|
127
|
+
if (separatorIndex <= 0) {
|
|
128
|
+
throw new Error(`invalid_env_assignment:${normalized}`);
|
|
129
|
+
}
|
|
130
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
131
|
+
const envValue = normalized.slice(separatorIndex + 1);
|
|
132
|
+
if (!key) {
|
|
133
|
+
throw new Error(`invalid_env_assignment:${normalized}`);
|
|
134
|
+
}
|
|
135
|
+
env[key] = envValue;
|
|
136
|
+
}
|
|
137
|
+
return env;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sanitizeIdSegment(value) {
|
|
141
|
+
return (
|
|
142
|
+
String(value || "")
|
|
143
|
+
.toLowerCase()
|
|
144
|
+
.replace(/[^a-z0-9]+/g, ".")
|
|
145
|
+
.replace(/^\.+|\.+$/g, "") || "project"
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function requestJson(baseUrl, pathname, { method = "GET", headers = {}, body } = {}) {
|
|
150
|
+
const response = await fetch(new URL(pathname, baseUrl), {
|
|
151
|
+
method,
|
|
152
|
+
headers: {
|
|
153
|
+
...headers,
|
|
154
|
+
...(body === undefined ? {} : { "content-type": "application/json; charset=utf-8" })
|
|
155
|
+
},
|
|
156
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
157
|
+
});
|
|
158
|
+
const text = await response.text();
|
|
159
|
+
return {
|
|
160
|
+
status: response.status,
|
|
161
|
+
body: text ? JSON.parse(text) : null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readSupervisorSessionToken() {
|
|
166
|
+
const session = readJsonFile(getOpsSessionFile(), null);
|
|
167
|
+
if (!session?.token || !session?.expires_at) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const expiresAt = Date.parse(session.expires_at);
|
|
171
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return String(session.token);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeSupervisorSessionToken(session) {
|
|
178
|
+
if (!session?.token || !session?.expires_at) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
ensureOpsDirectories();
|
|
182
|
+
const sessionFile = getOpsSessionFile();
|
|
183
|
+
fs.writeFileSync(
|
|
184
|
+
sessionFile,
|
|
185
|
+
`${JSON.stringify({ token: String(session.token), expires_at: String(session.expires_at) }, null, 2)}\n`,
|
|
186
|
+
"utf8"
|
|
187
|
+
);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function withSupervisorSessionHeaders(headers = {}) {
|
|
192
|
+
const token = readSupervisorSessionToken();
|
|
193
|
+
return token ? { ...headers, [OPS_SESSION_HEADER]: token } : headers;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function recoverSupervisorSession(state) {
|
|
197
|
+
const response = await requestJson(supervisorUrlFromState(state), "/auth/session");
|
|
198
|
+
const session = response.body?.recoverable_session;
|
|
199
|
+
if (!session?.token || !session?.expires_at) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return writeSupervisorSessionToken(session);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function requestSupervisorJson(state, pathname, options = {}) {
|
|
206
|
+
let response = await requestJson(supervisorUrlFromState(state), pathname, {
|
|
207
|
+
...options,
|
|
208
|
+
headers: withSupervisorSessionHeaders(options.headers || {})
|
|
209
|
+
});
|
|
210
|
+
if (response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
|
|
211
|
+
const sessionFile = getOpsSessionFile();
|
|
212
|
+
if (fs.existsSync(sessionFile)) {
|
|
213
|
+
try {
|
|
214
|
+
fs.rmSync(sessionFile, { force: true });
|
|
215
|
+
} catch {}
|
|
216
|
+
}
|
|
217
|
+
const recovered = await recoverSupervisorSession(state).catch(() => false);
|
|
218
|
+
if (recovered) {
|
|
219
|
+
response = await requestJson(supervisorUrlFromState(state), pathname, {
|
|
220
|
+
...options,
|
|
221
|
+
headers: withSupervisorSessionHeaders(options.headers || {})
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return response;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function runCliSubcommand(args, env) {
|
|
229
|
+
const result = await execFileAsync(process.execPath, [CLI_PATH, ...args], { env });
|
|
230
|
+
return result.stdout.trim() ? JSON.parse(result.stdout) : {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function waitFor(check, { timeoutMs = 15000, intervalMs = 250 } = {}) {
|
|
234
|
+
const started = Date.now();
|
|
235
|
+
while (Date.now() - started < timeoutMs) {
|
|
236
|
+
try {
|
|
237
|
+
return await check();
|
|
238
|
+
} catch {}
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
240
|
+
}
|
|
241
|
+
throw new Error("timeout");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parsePort(value, fallback) {
|
|
245
|
+
const parsed = Number(value);
|
|
246
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
247
|
+
return fallback;
|
|
248
|
+
}
|
|
249
|
+
return Math.trunc(parsed);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveUiConfig(args = {}) {
|
|
253
|
+
return {
|
|
254
|
+
host: String(args["ui-host"] || args.host || process.env.DELEXEC_OPS_UI_HOST || DEFAULT_CONSOLE_HOST).trim() || DEFAULT_CONSOLE_HOST,
|
|
255
|
+
port: parsePort(args["ui-port"] || args.port || process.env.DELEXEC_OPS_UI_PORT, DEFAULT_CONSOLE_PORT),
|
|
256
|
+
openBrowser: args["no-browser"] ? false : args["open-ui"] === true || args.open === true
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function uiUrl({ host, port }) {
|
|
261
|
+
return `http://${host}:${port}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function npmExecutable() {
|
|
265
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function canLaunchOpsConsoleWorkspace() {
|
|
269
|
+
return fs.existsSync(path.join(OPS_CONSOLE_DIR, "package.json"));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildUiLaunchCommand({ host, port }) {
|
|
273
|
+
const bin = process.env.DELEXEC_OPS_UI_BIN;
|
|
274
|
+
const customArgs = process.env.DELEXEC_OPS_UI_ARGS ? JSON.parse(process.env.DELEXEC_OPS_UI_ARGS) : null;
|
|
275
|
+
if (bin) {
|
|
276
|
+
return {
|
|
277
|
+
command: bin,
|
|
278
|
+
args: Array.isArray(customArgs) ? customArgs : [],
|
|
279
|
+
cwd: CLIENT_ROOT,
|
|
280
|
+
launch_mode: "configured_command"
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (!canLaunchOpsConsoleWorkspace()) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
"Ops Console UI requires a source checkout. Run from the delegated-execution-client workspace, or use delexec-ops status/debug-snapshot from the global CLI. See the README source install section."
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
command: npmExecutable(),
|
|
290
|
+
args: [
|
|
291
|
+
"exec",
|
|
292
|
+
"--workspace",
|
|
293
|
+
"@delexec/ops-console",
|
|
294
|
+
"--",
|
|
295
|
+
"vite",
|
|
296
|
+
"--host",
|
|
297
|
+
host,
|
|
298
|
+
"--port",
|
|
299
|
+
String(port),
|
|
300
|
+
"--strictPort"
|
|
301
|
+
],
|
|
302
|
+
cwd: CLIENT_ROOT,
|
|
303
|
+
launch_mode: "workspace_vite"
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function waitForUi(url) {
|
|
308
|
+
return waitFor(async () => {
|
|
309
|
+
const response = await fetch(url, { method: "GET" });
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
throw new Error("ui_not_ready");
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}, { timeoutMs: DEFAULT_UI_READY_TIMEOUT_MS, intervalMs: 500 });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function openBrowser(url) {
|
|
318
|
+
const configuredBin = process.env.DELEXEC_OPS_BROWSER_BIN;
|
|
319
|
+
const configuredArgs = process.env.DELEXEC_OPS_BROWSER_ARGS ? JSON.parse(process.env.DELEXEC_OPS_BROWSER_ARGS) : null;
|
|
320
|
+
if (configuredBin) {
|
|
321
|
+
const child = spawn(configuredBin, [...(Array.isArray(configuredArgs) ? configuredArgs : []), url], {
|
|
322
|
+
detached: true,
|
|
323
|
+
stdio: "ignore"
|
|
324
|
+
});
|
|
325
|
+
child.unref();
|
|
326
|
+
return { opened: true, command: configuredBin };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const browserCommand =
|
|
330
|
+
process.platform === "darwin"
|
|
331
|
+
? { command: "open", args: [url] }
|
|
332
|
+
: process.platform === "win32"
|
|
333
|
+
? { command: "cmd", args: ["/c", "start", "", url] }
|
|
334
|
+
: { command: "xdg-open", args: [url] };
|
|
335
|
+
|
|
336
|
+
const child = spawn(browserCommand.command, browserCommand.args, {
|
|
337
|
+
detached: true,
|
|
338
|
+
stdio: "ignore"
|
|
339
|
+
});
|
|
340
|
+
child.unref();
|
|
341
|
+
return { opened: true, command: browserCommand.command };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function ensureUiAvailable(args = {}, env = process.env) {
|
|
345
|
+
const ui = resolveUiConfig(args);
|
|
346
|
+
const consoleUrl = uiUrl(ui);
|
|
347
|
+
let alreadyRunning = false;
|
|
348
|
+
try {
|
|
349
|
+
const probe = await fetch(consoleUrl, { method: "GET", signal: AbortSignal.timeout(2000) });
|
|
350
|
+
alreadyRunning = probe.ok;
|
|
351
|
+
} catch {}
|
|
352
|
+
|
|
353
|
+
let pid = null;
|
|
354
|
+
let launchMode = "existing";
|
|
355
|
+
if (!alreadyRunning) {
|
|
356
|
+
const launch = buildUiLaunchCommand(ui);
|
|
357
|
+
const child = spawn(launch.command, launch.args, {
|
|
358
|
+
cwd: launch.cwd,
|
|
359
|
+
env: {
|
|
360
|
+
...env,
|
|
361
|
+
OPS_PORT_SUPERVISOR: String(env.OPS_PORT_SUPERVISOR || ensureOpsState().config.runtime.ports.supervisor),
|
|
362
|
+
DELEXEC_OPS_UI_HOST: ui.host,
|
|
363
|
+
DELEXEC_OPS_UI_PORT: String(ui.port)
|
|
364
|
+
},
|
|
365
|
+
detached: true,
|
|
366
|
+
stdio: "ignore"
|
|
367
|
+
});
|
|
368
|
+
child.unref();
|
|
369
|
+
pid = child.pid || null;
|
|
370
|
+
launchMode = launch.launch_mode;
|
|
371
|
+
await waitForUi(consoleUrl);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let browser = { opened: false };
|
|
375
|
+
if (ui.openBrowser) {
|
|
376
|
+
browser = openBrowser(consoleUrl);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
url: consoleUrl,
|
|
382
|
+
host: ui.host,
|
|
383
|
+
port: ui.port,
|
|
384
|
+
started: !alreadyRunning,
|
|
385
|
+
pid,
|
|
386
|
+
launch_mode: launchMode,
|
|
387
|
+
browser
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildResponderRegisterHeaders(state) {
|
|
392
|
+
const apiKey =
|
|
393
|
+
state.config.caller.api_key ||
|
|
394
|
+
state.env.CALLER_PLATFORM_API_KEY ||
|
|
395
|
+
state.env.PLATFORM_API_KEY ||
|
|
396
|
+
process.env.CALLER_PLATFORM_API_KEY ||
|
|
397
|
+
process.env.PLATFORM_API_KEY ||
|
|
398
|
+
state.env.RESPONDER_PLATFORM_API_KEY;
|
|
399
|
+
if (!apiKey) {
|
|
400
|
+
throw new Error("caller_platform_api_key_required");
|
|
401
|
+
}
|
|
402
|
+
return { Authorization: `Bearer ${apiKey}` };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function verifyRegisteredHotline(state, { hotlineId, expectedTemplateRef }) {
|
|
406
|
+
let detail;
|
|
407
|
+
let bundle;
|
|
408
|
+
try {
|
|
409
|
+
detail = await requestJson(state.config.platform.base_url, `/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}`, {
|
|
410
|
+
headers: buildResponderRegisterHeaders(state)
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
catalog_visible: false,
|
|
416
|
+
template_ref_matches: false,
|
|
417
|
+
template_bundle_available: false,
|
|
418
|
+
catalog_status: null,
|
|
419
|
+
template_bundle_status: null,
|
|
420
|
+
error: error instanceof Error ? error.message : "catalog_verification_failed"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const actualTemplateRef = detail.body?.template_ref || null;
|
|
425
|
+
const templateRefMatches = Boolean(detail.status === 200 && actualTemplateRef && actualTemplateRef === expectedTemplateRef);
|
|
426
|
+
if (detail.status === 200 && actualTemplateRef) {
|
|
427
|
+
try {
|
|
428
|
+
bundle = await requestJson(
|
|
429
|
+
state.config.platform.base_url,
|
|
430
|
+
`/v1/catalog/hotlines/${encodeURIComponent(hotlineId)}/template-bundle?template_ref=${encodeURIComponent(actualTemplateRef)}`,
|
|
431
|
+
{
|
|
432
|
+
headers: buildResponderRegisterHeaders(state)
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
bundle = {
|
|
437
|
+
status: null,
|
|
438
|
+
body: {
|
|
439
|
+
ok: false,
|
|
440
|
+
error: error instanceof Error ? error.message : "template_bundle_verification_failed"
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const templateBundleAvailable = Boolean(bundle?.status === 200);
|
|
447
|
+
return {
|
|
448
|
+
ok: Boolean(detail.status === 200 && templateRefMatches && templateBundleAvailable),
|
|
449
|
+
catalog_visible: detail.status === 200,
|
|
450
|
+
template_ref_matches: templateRefMatches,
|
|
451
|
+
template_bundle_available: templateBundleAvailable,
|
|
452
|
+
catalog_status: detail.status,
|
|
453
|
+
template_bundle_status: bundle?.status ?? null,
|
|
454
|
+
template_ref: actualTemplateRef || expectedTemplateRef || null
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function parseHotlineDefinition(args) {
|
|
459
|
+
const type = String(args.type || "process");
|
|
460
|
+
const hotlineId = String(args["hotline-id"] || "").trim();
|
|
461
|
+
if (!hotlineId) {
|
|
462
|
+
throw new Error("hotline_id_required");
|
|
463
|
+
}
|
|
464
|
+
const definition = {
|
|
465
|
+
hotline_id: hotlineId,
|
|
466
|
+
display_name: String(args["display-name"] || hotlineId),
|
|
467
|
+
enabled: true,
|
|
468
|
+
task_types: getValues(args["task-type"]),
|
|
469
|
+
capabilities: getValues(args.capability),
|
|
470
|
+
tags: getValues(args.tag),
|
|
471
|
+
adapter_type: type,
|
|
472
|
+
timeouts: {
|
|
473
|
+
soft_timeout_s: Number(args["soft-timeout-s"] || 60),
|
|
474
|
+
hard_timeout_s: Number(args["hard-timeout-s"] || 180)
|
|
475
|
+
},
|
|
476
|
+
review_status: "local_only",
|
|
477
|
+
submitted_for_review: false
|
|
478
|
+
};
|
|
479
|
+
if (type === "http") {
|
|
480
|
+
const url = String(args.url || "").trim();
|
|
481
|
+
if (!url) {
|
|
482
|
+
throw new Error("http_adapter_url_required");
|
|
483
|
+
}
|
|
484
|
+
definition.adapter = {
|
|
485
|
+
url,
|
|
486
|
+
method: String(args.method || "POST").toUpperCase()
|
|
487
|
+
};
|
|
488
|
+
return definition;
|
|
489
|
+
}
|
|
490
|
+
const cmd = String(args.cmd || "").trim();
|
|
491
|
+
if (!cmd) {
|
|
492
|
+
throw new Error("process_adapter_cmd_required");
|
|
493
|
+
}
|
|
494
|
+
definition.adapter = {
|
|
495
|
+
cmd,
|
|
496
|
+
cwd: args.cwd ? String(args.cwd) : undefined,
|
|
497
|
+
env: parseEnvAssignments(args.env)
|
|
498
|
+
};
|
|
499
|
+
return definition;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function buildProjectHotlineDefinition(args) {
|
|
503
|
+
const rawProjectPath = String(args["project-path"] || "").trim();
|
|
504
|
+
if (!rawProjectPath) {
|
|
505
|
+
throw new Error("project_path_required");
|
|
506
|
+
}
|
|
507
|
+
const projectPath = path.resolve(rawProjectPath);
|
|
508
|
+
if (!fs.existsSync(projectPath)) {
|
|
509
|
+
throw new Error("project_path_not_found");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const projectName = String(args["project-name"] || path.basename(projectPath) || "Local Project").trim();
|
|
513
|
+
const projectDescription = String(args["project-description"] || args.description || "").trim();
|
|
514
|
+
const adapterType = String(args.type || (args.url ? "http" : "process")).trim();
|
|
515
|
+
const hotlineId = String(args["hotline-id"] || `local.${sanitizeIdSegment(projectName)}.v1`).trim();
|
|
516
|
+
const tags = Array.from(new Set(["local", "project", ...getValues(args.tag)]));
|
|
517
|
+
const taskTypes = getValues(args["task-type"]);
|
|
518
|
+
const capabilities = getValues(args.capability);
|
|
519
|
+
|
|
520
|
+
const definition = {
|
|
521
|
+
hotline_id: hotlineId,
|
|
522
|
+
display_name: String(args["display-name"] || projectName).trim(),
|
|
523
|
+
enabled: true,
|
|
524
|
+
task_types: taskTypes.length > 0 ? taskTypes : ["project_task"],
|
|
525
|
+
capabilities: capabilities.length > 0 ? capabilities : [`project.${sanitizeIdSegment(projectName)}`],
|
|
526
|
+
tags,
|
|
527
|
+
adapter_type: adapterType,
|
|
528
|
+
timeouts: {
|
|
529
|
+
soft_timeout_s: Number(args["soft-timeout-s"] || 60),
|
|
530
|
+
hard_timeout_s: Number(args["hard-timeout-s"] || 180)
|
|
531
|
+
},
|
|
532
|
+
review_status: "local_only",
|
|
533
|
+
submitted_for_review: false,
|
|
534
|
+
metadata: {
|
|
535
|
+
project: {
|
|
536
|
+
path: projectPath,
|
|
537
|
+
name: projectName,
|
|
538
|
+
description: projectDescription || null,
|
|
539
|
+
mount_kind: "local_project"
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
if (adapterType === "http") {
|
|
545
|
+
const url = String(args.url || "").trim();
|
|
546
|
+
if (!url) {
|
|
547
|
+
throw new Error("http_adapter_url_required");
|
|
548
|
+
}
|
|
549
|
+
definition.adapter = {
|
|
550
|
+
url,
|
|
551
|
+
method: String(args.method || "POST").toUpperCase()
|
|
552
|
+
};
|
|
553
|
+
return definition;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const cmd = String(args.cmd || "").trim();
|
|
557
|
+
if (!cmd) {
|
|
558
|
+
throw new Error("process_adapter_cmd_required");
|
|
559
|
+
}
|
|
560
|
+
definition.adapter = {
|
|
561
|
+
cmd,
|
|
562
|
+
cwd: args.cwd ? String(args.cwd) : projectPath,
|
|
563
|
+
env: parseEnvAssignments(args.env)
|
|
564
|
+
};
|
|
565
|
+
return definition;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function supervisorUrlFromState(state) {
|
|
569
|
+
return `http://127.0.0.1:${state.config.runtime.ports.supervisor}`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function ensureSupervisorAvailable(baseUrl, env) {
|
|
573
|
+
try {
|
|
574
|
+
const health = await requestJson(baseUrl, "/healthz");
|
|
575
|
+
if (health.status === 200) {
|
|
576
|
+
return { started: false };
|
|
577
|
+
}
|
|
578
|
+
} catch {}
|
|
579
|
+
|
|
580
|
+
const child = spawn(process.execPath, [CLI_PATH, "start"], {
|
|
581
|
+
env,
|
|
582
|
+
detached: true,
|
|
583
|
+
stdio: "ignore"
|
|
584
|
+
});
|
|
585
|
+
child.unref();
|
|
586
|
+
|
|
587
|
+
await waitFor(async () => {
|
|
588
|
+
const health = await requestJson(baseUrl, "/healthz");
|
|
589
|
+
if (health.status !== 200) {
|
|
590
|
+
throw new Error("supervisor_not_ready");
|
|
591
|
+
}
|
|
592
|
+
return health;
|
|
593
|
+
});
|
|
594
|
+
return { started: true };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function maybeApproveExample({ platformUrl, adminApiKey, responderId }) {
|
|
598
|
+
if (!adminApiKey) {
|
|
599
|
+
return { ok: false, reason: "admin_api_key_missing" };
|
|
600
|
+
}
|
|
601
|
+
const headers = { Authorization: `Bearer ${adminApiKey}` };
|
|
602
|
+
const responder = await requestJson(platformUrl, `/v2/admin/responders/${encodeURIComponent(responderId)}/approve`, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers,
|
|
605
|
+
body: { reason: "ops bootstrap local demo approval" }
|
|
606
|
+
});
|
|
607
|
+
const hotline = await requestJson(platformUrl, `/v2/admin/hotlines/${encodeURIComponent(LOCAL_EXAMPLE_HOTLINE_ID)}/approve`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers,
|
|
610
|
+
body: { reason: "ops bootstrap local demo approval" }
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
ok: responder.status === 200 && hotline.status === 200,
|
|
614
|
+
responder,
|
|
615
|
+
hotline
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function waitForCatalogVisibility(supervisorUrl, responderId, options) {
|
|
620
|
+
return waitFor(async () => {
|
|
621
|
+
const catalog = await requestJson(
|
|
622
|
+
supervisorUrl,
|
|
623
|
+
`/catalog/hotlines?hotline_id=${encodeURIComponent(LOCAL_EXAMPLE_HOTLINE_ID)}&responder_id=${encodeURIComponent(responderId)}`
|
|
624
|
+
);
|
|
625
|
+
const item = catalog.body?.items?.find(
|
|
626
|
+
(entry) => entry.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID && entry.responder_id === responderId
|
|
627
|
+
);
|
|
628
|
+
if (!item) {
|
|
629
|
+
throw new Error("catalog_not_ready");
|
|
630
|
+
}
|
|
631
|
+
return item;
|
|
632
|
+
}, options);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function commandSetup(args = {}) {
|
|
636
|
+
const state = ensureOpsState();
|
|
637
|
+
ensureResponderIdentity(state, {
|
|
638
|
+
responderId: args["responder-id"] ? String(args["responder-id"]) : null,
|
|
639
|
+
displayName: args["display-name"] ? String(args["display-name"]) : null
|
|
640
|
+
});
|
|
641
|
+
state.env = saveOpsState(state);
|
|
642
|
+
emit({
|
|
643
|
+
ok: true,
|
|
644
|
+
ops_home: path.dirname(state.envFile),
|
|
645
|
+
env_file: state.envFile,
|
|
646
|
+
config_file: state.opsConfigFile,
|
|
647
|
+
config: state.config
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function commandStart() {
|
|
652
|
+
const state = ensureOpsState();
|
|
653
|
+
ensureResponderIdentity(state);
|
|
654
|
+
state.env = saveOpsState(state);
|
|
655
|
+
const server = createOpsSupervisorServer();
|
|
656
|
+
await new Promise((resolve) => server.listen(state.config.runtime.ports.supervisor, "127.0.0.1", resolve));
|
|
657
|
+
await server.startManagedServices();
|
|
658
|
+
console.log(`[ops-supervisor] listening on ${state.config.runtime.ports.supervisor}`);
|
|
659
|
+
server.on("close", () => {
|
|
660
|
+
void server.stopManagedServices();
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function commandStatus() {
|
|
665
|
+
const state = ensureOpsState();
|
|
666
|
+
try {
|
|
667
|
+
const response = await requestJson(supervisorUrlFromState(state), "/status");
|
|
668
|
+
emit(response.body);
|
|
669
|
+
} catch {
|
|
670
|
+
emit({
|
|
671
|
+
ok: false,
|
|
672
|
+
running: false,
|
|
673
|
+
config: state.config
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function commandMcpSpec() {
|
|
679
|
+
const state = ensureOpsState();
|
|
680
|
+
const response = await requestJson(supervisorUrlFromState(state), "/mcp-adapter/spec");
|
|
681
|
+
emit(response.body);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function commandAuthRegister(args) {
|
|
685
|
+
const state = ensureOpsState();
|
|
686
|
+
const localOnly = args.local === true;
|
|
687
|
+
if (args.platform) {
|
|
688
|
+
state.config.platform.base_url = String(args.platform).trim();
|
|
689
|
+
state.config.platform.enabled = true;
|
|
690
|
+
state.config.platform_console ||= {};
|
|
691
|
+
state.config.platform_console.base_url = state.config.platform.base_url;
|
|
692
|
+
state.env = saveOpsState(state);
|
|
693
|
+
}
|
|
694
|
+
const email = String(args.email || "").trim();
|
|
695
|
+
if (!email) {
|
|
696
|
+
throw new Error("email_required");
|
|
697
|
+
}
|
|
698
|
+
const fallbackRegister = async () => {
|
|
699
|
+
const local = ensureOpsState();
|
|
700
|
+
local.config.platform.base_url = String(args.platform || local.config.platform.base_url).trim();
|
|
701
|
+
local.config.platform.enabled = true;
|
|
702
|
+
local.config.platform_console ||= {};
|
|
703
|
+
local.config.platform_console.base_url = local.config.platform.base_url;
|
|
704
|
+
const direct = await requestJson(local.config.platform.base_url, "/v1/users/register", {
|
|
705
|
+
method: "POST",
|
|
706
|
+
body: { contact_email: email }
|
|
707
|
+
});
|
|
708
|
+
if (direct.status === 201) {
|
|
709
|
+
local.config.caller.api_key = direct.body.api_key;
|
|
710
|
+
local.config.caller.contact_email = direct.body.contact_email || email;
|
|
711
|
+
local.env = saveOpsState(local);
|
|
712
|
+
}
|
|
713
|
+
return direct;
|
|
714
|
+
};
|
|
715
|
+
let response;
|
|
716
|
+
try {
|
|
717
|
+
response = await requestSupervisorJson(state, "/auth/register-caller", {
|
|
718
|
+
method: "POST",
|
|
719
|
+
body: {
|
|
720
|
+
contact_email: email,
|
|
721
|
+
mode: localOnly ? "local_only" : "platform"
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
if (!localOnly && response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
|
|
725
|
+
response = await fallbackRegister();
|
|
726
|
+
}
|
|
727
|
+
} catch {
|
|
728
|
+
response = localOnly
|
|
729
|
+
? (() => {
|
|
730
|
+
state.config.caller.contact_email = email;
|
|
731
|
+
state.config.caller.registration_mode = "local_only";
|
|
732
|
+
state.config.caller.api_key = null;
|
|
733
|
+
state.config.caller.api_key_configured = false;
|
|
734
|
+
state.env = saveOpsState(state);
|
|
735
|
+
return {
|
|
736
|
+
status: 201,
|
|
737
|
+
body: {
|
|
738
|
+
ok: true,
|
|
739
|
+
registered: true,
|
|
740
|
+
mode: "local_only",
|
|
741
|
+
contact_email: email
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
})()
|
|
745
|
+
: await fallbackRegister();
|
|
746
|
+
}
|
|
747
|
+
emit({
|
|
748
|
+
ok: response.status === 201,
|
|
749
|
+
...response.body
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function commandEnableResponder(args) {
|
|
754
|
+
const state = ensureOpsState();
|
|
755
|
+
state.config.responder.enabled = true;
|
|
756
|
+
ensureResponderIdentity(state, {
|
|
757
|
+
responderId: args["responder-id"] ? String(args["responder-id"]) : null,
|
|
758
|
+
displayName: args["display-name"] ? String(args["display-name"]) : null
|
|
759
|
+
});
|
|
760
|
+
state.env = saveOpsState(state);
|
|
761
|
+
try {
|
|
762
|
+
const response = await requestSupervisorJson(state, "/responder/enable", {
|
|
763
|
+
method: "POST",
|
|
764
|
+
body: {
|
|
765
|
+
responder_id: state.config.responder.responder_id,
|
|
766
|
+
display_name: state.config.responder.display_name
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
emit(response.body);
|
|
770
|
+
} catch {
|
|
771
|
+
emit({
|
|
772
|
+
ok: true,
|
|
773
|
+
responder: state.config.responder,
|
|
774
|
+
submitted: 0,
|
|
775
|
+
review: null
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function commandAddHotline(args) {
|
|
781
|
+
const state = ensureOpsState();
|
|
782
|
+
const definition = parseHotlineDefinition(args);
|
|
783
|
+
const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
|
|
784
|
+
upsertHotline(state, definition);
|
|
785
|
+
state.env = saveOpsState(state);
|
|
786
|
+
let runtime = { synced: false, auth_required: false };
|
|
787
|
+
try {
|
|
788
|
+
const response = await requestSupervisorJson(state, "/responder/hotlines", {
|
|
789
|
+
method: "POST",
|
|
790
|
+
body: definition
|
|
791
|
+
});
|
|
792
|
+
if (response.status === 201) {
|
|
793
|
+
runtime = {
|
|
794
|
+
synced: true,
|
|
795
|
+
auth_required: false,
|
|
796
|
+
...(response.body?.runtime || {})
|
|
797
|
+
};
|
|
798
|
+
} else if (response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
|
|
799
|
+
runtime = { synced: false, auth_required: true };
|
|
800
|
+
}
|
|
801
|
+
} catch {}
|
|
802
|
+
emit({
|
|
803
|
+
ok: true,
|
|
804
|
+
hotline_id: definition.hotline_id,
|
|
805
|
+
adapter_type: definition.adapter_type,
|
|
806
|
+
runtime,
|
|
807
|
+
local_integration_file: registrationDraft.integration_file,
|
|
808
|
+
local_hook_file: registrationDraft.hook_file,
|
|
809
|
+
registration_draft_file: registrationDraft.draft_file,
|
|
810
|
+
registration_draft: registrationDraft.draft
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function commandAttachProject(args) {
|
|
815
|
+
const state = ensureOpsState();
|
|
816
|
+
const definition = buildProjectHotlineDefinition(args);
|
|
817
|
+
const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
|
|
818
|
+
upsertHotline(state, definition);
|
|
819
|
+
state.env = saveOpsState(state);
|
|
820
|
+
let runtime = { synced: false, auth_required: false };
|
|
821
|
+
try {
|
|
822
|
+
const response = await requestSupervisorJson(state, "/responder/hotlines", {
|
|
823
|
+
method: "POST",
|
|
824
|
+
body: definition
|
|
825
|
+
});
|
|
826
|
+
if (response.status === 201) {
|
|
827
|
+
runtime = {
|
|
828
|
+
synced: true,
|
|
829
|
+
auth_required: false,
|
|
830
|
+
...(response.body?.runtime || {})
|
|
831
|
+
};
|
|
832
|
+
} else if (response.status === 401 && response.body?.error?.code === "AUTH_SESSION_REQUIRED") {
|
|
833
|
+
runtime = { synced: false, auth_required: true };
|
|
834
|
+
}
|
|
835
|
+
} catch {}
|
|
836
|
+
emit({
|
|
837
|
+
ok: true,
|
|
838
|
+
hotline_id: definition.hotline_id,
|
|
839
|
+
adapter_type: definition.adapter_type,
|
|
840
|
+
project: definition.metadata.project,
|
|
841
|
+
runtime,
|
|
842
|
+
local_integration_file: registrationDraft.integration_file,
|
|
843
|
+
local_hook_file: registrationDraft.hook_file,
|
|
844
|
+
registration_draft_file: registrationDraft.draft_file,
|
|
845
|
+
registration_draft: registrationDraft.draft
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function commandAddExampleHotline() {
|
|
850
|
+
const state = ensureOpsState();
|
|
851
|
+
const existing = (state.config.responder.hotlines || []).find((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
|
|
852
|
+
const stale = isExampleHotlineDefinitionStale(existing);
|
|
853
|
+
const definition = buildExampleHotlineDefinition(existing);
|
|
854
|
+
if (stale) {
|
|
855
|
+
definition.submitted_for_review = false;
|
|
856
|
+
definition.review_status = "local_only";
|
|
857
|
+
}
|
|
858
|
+
const registrationDraft = ensureHotlineRegistrationDraft(state, definition);
|
|
859
|
+
upsertHotline(state, definition);
|
|
860
|
+
state.env = saveOpsState(state);
|
|
861
|
+
try {
|
|
862
|
+
const response = await requestJson(supervisorUrlFromState(state), "/responder/hotlines/example", {
|
|
863
|
+
method: "POST",
|
|
864
|
+
body: {}
|
|
865
|
+
});
|
|
866
|
+
emit(response.body);
|
|
867
|
+
return;
|
|
868
|
+
} catch {}
|
|
869
|
+
emit({
|
|
870
|
+
ok: true,
|
|
871
|
+
example: true,
|
|
872
|
+
hotline_id: definition.hotline_id,
|
|
873
|
+
adapter_type: definition.adapter_type,
|
|
874
|
+
local_integration_file: registrationDraft.integration_file,
|
|
875
|
+
local_hook_file: registrationDraft.hook_file,
|
|
876
|
+
registration_draft_file: registrationDraft.draft_file,
|
|
877
|
+
registration_draft: registrationDraft.draft
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function commandSetHotlineEnabled(args, enabled) {
|
|
882
|
+
const state = ensureOpsState();
|
|
883
|
+
const hotlineId = String(args["hotline-id"] || "").trim();
|
|
884
|
+
if (!hotlineId) {
|
|
885
|
+
throw new Error("hotline_id_required");
|
|
886
|
+
}
|
|
887
|
+
const item = setHotlineEnabled(state, hotlineId, enabled);
|
|
888
|
+
if (!item) {
|
|
889
|
+
throw new Error("hotline_not_found");
|
|
890
|
+
}
|
|
891
|
+
state.env = saveOpsState(state);
|
|
892
|
+
try {
|
|
893
|
+
const response = await requestSupervisorJson(
|
|
894
|
+
state,
|
|
895
|
+
`/responder/hotlines/${encodeURIComponent(hotlineId)}/${enabled ? "enable" : "disable"}`,
|
|
896
|
+
{
|
|
897
|
+
method: "POST",
|
|
898
|
+
body: {}
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
emit(response.body);
|
|
902
|
+
return;
|
|
903
|
+
} catch {}
|
|
904
|
+
emit({
|
|
905
|
+
ok: true,
|
|
906
|
+
hotline_id: item.hotline_id,
|
|
907
|
+
enabled: item.enabled !== false
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function commandRemoveHotline(args) {
|
|
912
|
+
const state = ensureOpsState();
|
|
913
|
+
const hotlineId = String(args["hotline-id"] || "").trim();
|
|
914
|
+
if (!hotlineId) {
|
|
915
|
+
throw new Error("hotline_id_required");
|
|
916
|
+
}
|
|
917
|
+
const item = removeHotline(state, hotlineId);
|
|
918
|
+
if (!item) {
|
|
919
|
+
throw new Error("hotline_not_found");
|
|
920
|
+
}
|
|
921
|
+
state.env = saveOpsState(state);
|
|
922
|
+
try {
|
|
923
|
+
const response = await requestSupervisorJson(state, `/responder/hotlines/${encodeURIComponent(hotlineId)}`, {
|
|
924
|
+
method: "DELETE"
|
|
925
|
+
});
|
|
926
|
+
emit(response.body);
|
|
927
|
+
return;
|
|
928
|
+
} catch {}
|
|
929
|
+
emit({
|
|
930
|
+
ok: true,
|
|
931
|
+
removed: {
|
|
932
|
+
hotline_id: item.hotline_id
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function requireHotlineIdArg(args) {
|
|
938
|
+
const hotlineId = String(args["hotline-id"] || "").trim();
|
|
939
|
+
if (!hotlineId) {
|
|
940
|
+
throw new Error("hotline_id_required");
|
|
941
|
+
}
|
|
942
|
+
return hotlineId;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function commandShowDraft(args = {}) {
|
|
946
|
+
const state = ensureOpsState();
|
|
947
|
+
const hotlineId = requireHotlineIdArg(args);
|
|
948
|
+
const hotline = (state.config.responder.hotlines || []).find((item) => item.hotline_id === hotlineId);
|
|
949
|
+
if (!hotline) {
|
|
950
|
+
throw new Error("hotline_not_found");
|
|
951
|
+
}
|
|
952
|
+
try {
|
|
953
|
+
const response = await requestSupervisorJson(state, `/responder/hotlines/${encodeURIComponent(hotlineId)}/draft`);
|
|
954
|
+
if (response.status === 200) {
|
|
955
|
+
emit(response.body);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
} catch {}
|
|
959
|
+
|
|
960
|
+
const registrationDraft = loadHotlineRegistrationDraft(state, hotline);
|
|
961
|
+
emit({
|
|
962
|
+
ok: Boolean(registrationDraft.draft),
|
|
963
|
+
hotline_id: hotline.hotline_id,
|
|
964
|
+
review_status: hotline.review_status || "local_only",
|
|
965
|
+
submitted_for_review: hotline.submitted_for_review === true,
|
|
966
|
+
local_integration_file: hotline?.metadata?.local?.integration_file || null,
|
|
967
|
+
local_hook_file: hotline?.metadata?.local?.hook_file || null,
|
|
968
|
+
draft_file: registrationDraft.draft_file,
|
|
969
|
+
draft: registrationDraft.draft
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function commandSubmitReview(args = {}) {
|
|
974
|
+
const state = ensureOpsState();
|
|
975
|
+
const hotlineId = args["hotline-id"] ? requireHotlineIdArg(args) : null;
|
|
976
|
+
if (hotlineId) {
|
|
977
|
+
const hotline = (state.config.responder.hotlines || []).find((item) => item.hotline_id === hotlineId);
|
|
978
|
+
if (!hotline) {
|
|
979
|
+
throw new Error("hotline_not_found");
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const responderIdentity = ensureResponderIdentity(state, {
|
|
983
|
+
responderId: args["responder-id"] ? String(args["responder-id"]) : null,
|
|
984
|
+
displayName: args["display-name"] ? String(args["display-name"]) : null
|
|
985
|
+
});
|
|
986
|
+
state.env = saveOpsState(state);
|
|
987
|
+
const pending = (state.config.responder.hotlines || []).filter(
|
|
988
|
+
(item) => item.submitted_for_review !== true && (!hotlineId || item.hotline_id === hotlineId)
|
|
989
|
+
);
|
|
990
|
+
for (const item of pending) {
|
|
991
|
+
try {
|
|
992
|
+
buildHotlineOnboardingBody(state, item, responderIdentity);
|
|
993
|
+
} catch (error) {
|
|
994
|
+
emit(buildStructuredError(
|
|
995
|
+
error?.code || "HOTLINE_DRAFT_INVALID",
|
|
996
|
+
error instanceof Error ? error.message : "hotline registration draft is invalid",
|
|
997
|
+
{ fields: Array.isArray(error?.fields) ? error.fields : [] }
|
|
998
|
+
));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
const response = await requestSupervisorJson(state, "/responder/submit-review", {
|
|
1004
|
+
method: "POST",
|
|
1005
|
+
body: {
|
|
1006
|
+
responder_id: state.config.responder.responder_id,
|
|
1007
|
+
display_name: state.config.responder.display_name,
|
|
1008
|
+
hotline_id: hotlineId
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
if (response.status === 201) {
|
|
1012
|
+
emit(response.body);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
} catch {}
|
|
1016
|
+
const results = [];
|
|
1017
|
+
for (const item of pending) {
|
|
1018
|
+
let onboarding;
|
|
1019
|
+
try {
|
|
1020
|
+
onboarding = buildHotlineOnboardingBody(state, item, responderIdentity);
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
emit(buildStructuredError(
|
|
1023
|
+
error?.code || "HOTLINE_DRAFT_INVALID",
|
|
1024
|
+
error instanceof Error ? error.message : "hotline registration draft is invalid",
|
|
1025
|
+
{ fields: Array.isArray(error?.fields) ? error.fields : [] }
|
|
1026
|
+
));
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const response = await requestJson(state.config.platform.base_url, "/v2/hotlines", {
|
|
1030
|
+
method: "POST",
|
|
1031
|
+
headers: buildResponderRegisterHeaders(state),
|
|
1032
|
+
body: onboarding.body
|
|
1033
|
+
});
|
|
1034
|
+
if (response.status !== 201) {
|
|
1035
|
+
emit(response.body);
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
state.env.RESPONDER_PLATFORM_API_KEY = response.body.responder_api_key || response.body.api_key;
|
|
1039
|
+
item.submitted_for_review = true;
|
|
1040
|
+
item.review_status = response.body.hotline_review_status || response.body.review_status || "pending";
|
|
1041
|
+
const verification = await verifyRegisteredHotline(state, {
|
|
1042
|
+
hotlineId: item.hotline_id,
|
|
1043
|
+
expectedTemplateRef: onboarding.body.template_ref
|
|
1044
|
+
});
|
|
1045
|
+
results.push({
|
|
1046
|
+
...response.body,
|
|
1047
|
+
draft_file: onboarding.draft_file,
|
|
1048
|
+
used_draft: onboarding.used_draft,
|
|
1049
|
+
verification
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
state.env = saveOpsState(state);
|
|
1053
|
+
emit({
|
|
1054
|
+
ok: true,
|
|
1055
|
+
responder_id: state.config.responder.responder_id,
|
|
1056
|
+
submitted: results.length,
|
|
1057
|
+
results
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function commandDoctor() {
|
|
1062
|
+
const state = ensureOpsState();
|
|
1063
|
+
const adapterChecks = (state.config.responder.hotlines || []).map((item) => {
|
|
1064
|
+
if (item.adapter_type === "http") {
|
|
1065
|
+
const valid = typeof item.adapter?.url === "string" && item.adapter.url.startsWith("http");
|
|
1066
|
+
return {
|
|
1067
|
+
hotline_id: item.hotline_id,
|
|
1068
|
+
adapter_type: item.adapter_type,
|
|
1069
|
+
ok: valid,
|
|
1070
|
+
detail: valid ? item.adapter.url : "invalid_http_url"
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const cmd = String(item.adapter?.cmd || "").trim();
|
|
1074
|
+
const firstToken = cmd.split(/\s+/).filter(Boolean)[0] || "";
|
|
1075
|
+
const isAbsolute = firstToken.startsWith("/");
|
|
1076
|
+
const valid = Boolean(cmd) && (!isAbsolute || fs.existsSync(firstToken));
|
|
1077
|
+
return {
|
|
1078
|
+
hotline_id: item.hotline_id,
|
|
1079
|
+
adapter_type: item.adapter_type || "process",
|
|
1080
|
+
ok: valid,
|
|
1081
|
+
detail: valid ? cmd : "process_command_missing_or_not_found"
|
|
1082
|
+
};
|
|
1083
|
+
});
|
|
1084
|
+
try {
|
|
1085
|
+
const response = await requestJson(supervisorUrlFromState(state), "/status");
|
|
1086
|
+
emit({
|
|
1087
|
+
ok: true,
|
|
1088
|
+
checks: response.body.runtime,
|
|
1089
|
+
debug: response.body.debug,
|
|
1090
|
+
adapters: adapterChecks
|
|
1091
|
+
});
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
emit({
|
|
1094
|
+
ok: false,
|
|
1095
|
+
message: error instanceof Error ? error.message : "unknown_error",
|
|
1096
|
+
config: state.config,
|
|
1097
|
+
adapters: adapterChecks
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function commandDebugSnapshot() {
|
|
1103
|
+
const state = ensureOpsState();
|
|
1104
|
+
const response = await requestSupervisorJson(state, "/debug/snapshot");
|
|
1105
|
+
emit(response.body);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function commandRunExample(args) {
|
|
1109
|
+
const state = ensureOpsState();
|
|
1110
|
+
const text = String(args.text || "Summarize this local example request.").trim();
|
|
1111
|
+
const response = await requestSupervisorJson(state, "/requests/example", {
|
|
1112
|
+
method: "POST",
|
|
1113
|
+
body: { text }
|
|
1114
|
+
});
|
|
1115
|
+
if (response.status !== 201 || !response.body?.request_id) {
|
|
1116
|
+
emit({
|
|
1117
|
+
ok: false,
|
|
1118
|
+
...response.body
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const requestId = response.body.request_id;
|
|
1123
|
+
const final = await waitFor(async () => {
|
|
1124
|
+
const current = await requestSupervisorJson(state, `/requests/${encodeURIComponent(requestId)}`);
|
|
1125
|
+
if (!["SUCCEEDED", "FAILED", "UNVERIFIED", "TIMED_OUT"].includes(current.body?.status)) {
|
|
1126
|
+
throw new Error("request_not_ready");
|
|
1127
|
+
}
|
|
1128
|
+
return current.body;
|
|
1129
|
+
});
|
|
1130
|
+
emit({
|
|
1131
|
+
ok: final.status === "SUCCEEDED",
|
|
1132
|
+
...response.body,
|
|
1133
|
+
status: final.status,
|
|
1134
|
+
request: final,
|
|
1135
|
+
result_package: final.result_package || null
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async function commandBootstrap(args) {
|
|
1140
|
+
const steps = [];
|
|
1141
|
+
const initialState = ensureOpsState();
|
|
1142
|
+
const setupArgs = ["setup"];
|
|
1143
|
+
if (args["responder-id"]) {
|
|
1144
|
+
setupArgs.push("--responder-id", String(args["responder-id"]));
|
|
1145
|
+
}
|
|
1146
|
+
if (args["display-name"]) {
|
|
1147
|
+
setupArgs.push("--display-name", String(args["display-name"]));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const explicitPlatformUrl = typeof args.platform === "string" ? args.platform.trim() : "";
|
|
1151
|
+
const platformUrl = String(explicitPlatformUrl || initialState.config.platform.base_url || "http://127.0.0.1:8080").trim();
|
|
1152
|
+
const bootstrapUsesPlatform = Boolean(explicitPlatformUrl);
|
|
1153
|
+
const warnings = [];
|
|
1154
|
+
const env = { ...process.env };
|
|
1155
|
+
if (bootstrapUsesPlatform) {
|
|
1156
|
+
env.PLATFORM_API_BASE_URL = platformUrl;
|
|
1157
|
+
} else {
|
|
1158
|
+
if (process.env.PLATFORM_API_BASE_URL) {
|
|
1159
|
+
warnings.push("PLATFORM_API_BASE_URL ignored for bootstrap; pass --platform to enable platform mode.");
|
|
1160
|
+
}
|
|
1161
|
+
delete env.PLATFORM_API_BASE_URL;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
try {
|
|
1165
|
+
const setup = await runCliSubcommand(setupArgs, env);
|
|
1166
|
+
logBootstrapStep(steps, "setup_ok", true, { ops_home: setup.ops_home });
|
|
1167
|
+
|
|
1168
|
+
let state = ensureOpsState();
|
|
1169
|
+
if (bootstrapUsesPlatform) {
|
|
1170
|
+
state.config.platform.enabled = true;
|
|
1171
|
+
state.config.platform.base_url = platformUrl;
|
|
1172
|
+
state.config.platform_console ||= {};
|
|
1173
|
+
state.config.platform_console.base_url = platformUrl;
|
|
1174
|
+
state.env = saveOpsState(state);
|
|
1175
|
+
logBootstrapStep(steps, "platform_enabled", true, { platform_url: platformUrl });
|
|
1176
|
+
}
|
|
1177
|
+
const email =
|
|
1178
|
+
String(args.email || state.config.caller.contact_email || process.env.BOOTSTRAP_CALLER_EMAIL || "").trim() ||
|
|
1179
|
+
`ops-user-${Date.now()}@local.test`;
|
|
1180
|
+
if (state.config.caller.api_key && state.config.caller.contact_email) {
|
|
1181
|
+
logBootstrapStep(steps, "caller_registered", true, {
|
|
1182
|
+
caller_email: state.config.caller.contact_email,
|
|
1183
|
+
existing: true
|
|
1184
|
+
});
|
|
1185
|
+
} else {
|
|
1186
|
+
const registerArgs = bootstrapUsesPlatform
|
|
1187
|
+
? ["auth", "register", "--email", email, "--platform", platformUrl]
|
|
1188
|
+
: ["auth", "register", "--email", email, "--local"];
|
|
1189
|
+
const register = await runCliSubcommand(registerArgs, env);
|
|
1190
|
+
logBootstrapStep(steps, "caller_registered", register.ok === true, {
|
|
1191
|
+
caller_email: register.contact_email || email,
|
|
1192
|
+
mode: register.mode || (bootstrapUsesPlatform ? "platform" : "local_only")
|
|
1193
|
+
});
|
|
1194
|
+
if (register.ok !== true) {
|
|
1195
|
+
emit({
|
|
1196
|
+
ok: false,
|
|
1197
|
+
stage: "caller_register_failed",
|
|
1198
|
+
steps,
|
|
1199
|
+
response: register
|
|
1200
|
+
});
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
state = ensureOpsState();
|
|
1206
|
+
const hasExample = (state.config.responder.hotlines || []).some((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
|
|
1207
|
+
const added = await runCliSubcommand(["add-example-hotline"], env);
|
|
1208
|
+
logBootstrapStep(steps, "example_hotline_added", added.ok !== false, {
|
|
1209
|
+
hotline_id: added.hotline_id || LOCAL_EXAMPLE_HOTLINE_ID,
|
|
1210
|
+
existing: hasExample,
|
|
1211
|
+
refreshed: hasExample
|
|
1212
|
+
});
|
|
1213
|
+
if (added.ok === false) {
|
|
1214
|
+
emit({
|
|
1215
|
+
ok: false,
|
|
1216
|
+
stage: "example_hotline_add_failed",
|
|
1217
|
+
steps,
|
|
1218
|
+
response: added
|
|
1219
|
+
});
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
state = ensureOpsState();
|
|
1224
|
+
const example = (state.config.responder.hotlines || []).find((item) => item.hotline_id === LOCAL_EXAMPLE_HOTLINE_ID);
|
|
1225
|
+
if (bootstrapUsesPlatform) {
|
|
1226
|
+
if (example?.submitted_for_review === true) {
|
|
1227
|
+
logBootstrapStep(steps, "review_submitted", true, {
|
|
1228
|
+
submitted: 0,
|
|
1229
|
+
existing: true
|
|
1230
|
+
});
|
|
1231
|
+
} else {
|
|
1232
|
+
const review = await runCliSubcommand(["submit-review"], env);
|
|
1233
|
+
const reviewOk = review.ok === true || typeof review.submitted === "number";
|
|
1234
|
+
logBootstrapStep(steps, "review_submitted", reviewOk, {
|
|
1235
|
+
submitted: review.submitted || 0
|
|
1236
|
+
});
|
|
1237
|
+
if (!reviewOk) {
|
|
1238
|
+
emit({
|
|
1239
|
+
ok: false,
|
|
1240
|
+
stage: "submit_review_failed",
|
|
1241
|
+
steps,
|
|
1242
|
+
response: review
|
|
1243
|
+
});
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} else {
|
|
1248
|
+
logBootstrapStep(steps, "review_skipped", true, {
|
|
1249
|
+
mode: "local_only"
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const enabled = await runCliSubcommand(["enable-responder"], env);
|
|
1254
|
+
const responderId = enabled.responder?.responder_id || enabled.responder_id || ensureOpsState().config.responder.responder_id;
|
|
1255
|
+
logBootstrapStep(steps, "responder_enabled", enabled.ok === true, { responder_id: responderId });
|
|
1256
|
+
if (enabled.ok !== true) {
|
|
1257
|
+
emit({
|
|
1258
|
+
ok: false,
|
|
1259
|
+
stage: "enable_responder_failed",
|
|
1260
|
+
steps,
|
|
1261
|
+
response: enabled
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const supervisorUrl = supervisorUrlFromState(ensureOpsState());
|
|
1267
|
+
const supervisor = await ensureSupervisorAvailable(supervisorUrl, env);
|
|
1268
|
+
logBootstrapStep(steps, "supervisor_started", true, supervisor);
|
|
1269
|
+
const supervisorSetup = await requestJson(supervisorUrl, "/setup", {
|
|
1270
|
+
method: "POST",
|
|
1271
|
+
body: {}
|
|
1272
|
+
});
|
|
1273
|
+
logBootstrapStep(steps, "supervisor_config_synced", supervisorSetup.status === 200);
|
|
1274
|
+
if (supervisorSetup.status !== 200) {
|
|
1275
|
+
emit({
|
|
1276
|
+
ok: false,
|
|
1277
|
+
stage: "supervisor_config_sync_failed",
|
|
1278
|
+
steps,
|
|
1279
|
+
response: supervisorSetup.body || supervisorSetup
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const adminApiKey = process.env.PLATFORM_ADMIN_API_KEY || process.env.ADMIN_API_KEY || null;
|
|
1285
|
+
if (bootstrapUsesPlatform) {
|
|
1286
|
+
let catalogVisible = null;
|
|
1287
|
+
try {
|
|
1288
|
+
catalogVisible = await waitForCatalogVisibility(supervisorUrl, responderId, {
|
|
1289
|
+
timeoutMs: 750,
|
|
1290
|
+
intervalMs: 150
|
|
1291
|
+
});
|
|
1292
|
+
} catch {}
|
|
1293
|
+
|
|
1294
|
+
if (adminApiKey) {
|
|
1295
|
+
const approved = await maybeApproveExample({
|
|
1296
|
+
platformUrl,
|
|
1297
|
+
adminApiKey,
|
|
1298
|
+
responderId
|
|
1299
|
+
});
|
|
1300
|
+
if (!approved.ok && !catalogVisible) {
|
|
1301
|
+
emit({
|
|
1302
|
+
ok: false,
|
|
1303
|
+
stage: "awaiting_admin_approval",
|
|
1304
|
+
steps,
|
|
1305
|
+
responder_id: responderId,
|
|
1306
|
+
hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
|
|
1307
|
+
next_action: "Approve the responder and hotline runtime, then rerun delexec-ops bootstrap or delexec-ops run-example.",
|
|
1308
|
+
reason: approved.reason || "approval_failed"
|
|
1309
|
+
});
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (approved.ok) {
|
|
1313
|
+
logBootstrapStep(steps, "responder_approved", true);
|
|
1314
|
+
logBootstrapStep(steps, "hotline_approved", true);
|
|
1315
|
+
}
|
|
1316
|
+
catalogVisible = await waitForCatalogVisibility(supervisorUrl, responderId, {
|
|
1317
|
+
timeoutMs: 15000,
|
|
1318
|
+
intervalMs: 250
|
|
1319
|
+
});
|
|
1320
|
+
} else if (!catalogVisible) {
|
|
1321
|
+
emit({
|
|
1322
|
+
ok: false,
|
|
1323
|
+
stage: "awaiting_admin_approval",
|
|
1324
|
+
steps,
|
|
1325
|
+
warnings,
|
|
1326
|
+
responder_id: responderId,
|
|
1327
|
+
hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
|
|
1328
|
+
next_action: "Approve the responder and hotline runtime, then rerun delexec-ops bootstrap or delexec-ops run-example.",
|
|
1329
|
+
reason: "admin_api_key_missing"
|
|
1330
|
+
});
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
logBootstrapStep(steps, "catalog_visible", true, { hotline_id: LOCAL_EXAMPLE_HOTLINE_ID });
|
|
1334
|
+
} else {
|
|
1335
|
+
logBootstrapStep(steps, "local_hotline_ready", true, { hotline_id: LOCAL_EXAMPLE_HOTLINE_ID });
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const started = await requestJson(supervisorUrl, "/requests/example", {
|
|
1339
|
+
method: "POST",
|
|
1340
|
+
body: {
|
|
1341
|
+
text: String(args.text || process.env.BOOTSTRAP_EXAMPLE_TEXT || "Summarize this bootstrap request.").trim()
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
if (started.status !== 201 || !started.body?.request_id) {
|
|
1345
|
+
if (bootstrapUsesPlatform && !adminApiKey) {
|
|
1346
|
+
emit({
|
|
1347
|
+
ok: false,
|
|
1348
|
+
stage: "awaiting_admin_approval",
|
|
1349
|
+
steps,
|
|
1350
|
+
responder_id: responderId,
|
|
1351
|
+
hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
|
|
1352
|
+
next_action: "Approve the responder and hotline runtime, then rerun delexec-ops bootstrap or delexec-ops run-example.",
|
|
1353
|
+
reason: started.body?.error?.code || "request_not_callable"
|
|
1354
|
+
});
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
emit({
|
|
1358
|
+
ok: false,
|
|
1359
|
+
stage: "request_start_failed",
|
|
1360
|
+
steps,
|
|
1361
|
+
warnings,
|
|
1362
|
+
response: started.body || started
|
|
1363
|
+
});
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const requestId = started.body.request_id;
|
|
1368
|
+
const final = await waitFor(async () => {
|
|
1369
|
+
const current = await requestJson(supervisorUrl, `/requests/${encodeURIComponent(requestId)}`);
|
|
1370
|
+
if (!["SUCCEEDED", "FAILED", "UNVERIFIED", "TIMED_OUT"].includes(current.body?.status)) {
|
|
1371
|
+
throw new Error("request_not_ready");
|
|
1372
|
+
}
|
|
1373
|
+
return current.body;
|
|
1374
|
+
});
|
|
1375
|
+
logBootstrapStep(steps, "request_succeeded", final.status === "SUCCEEDED", {
|
|
1376
|
+
request_id: requestId,
|
|
1377
|
+
status: final.status
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
emit({
|
|
1381
|
+
ok: final.status === "SUCCEEDED",
|
|
1382
|
+
request_id: requestId,
|
|
1383
|
+
status: final.status,
|
|
1384
|
+
responder_id: responderId,
|
|
1385
|
+
hotline_id: LOCAL_EXAMPLE_HOTLINE_ID,
|
|
1386
|
+
supervisor_url: supervisorUrl,
|
|
1387
|
+
ui: args["open-ui"] ? await ensureUiAvailable(args, env) : null,
|
|
1388
|
+
warnings,
|
|
1389
|
+
next_steps: {
|
|
1390
|
+
one_click_start: "delexec-ops bootstrap --open-ui",
|
|
1391
|
+
reopen_web_ui: "delexec-ops ui start --open",
|
|
1392
|
+
local_services: "delexec-ops start",
|
|
1393
|
+
health_check: "delexec-ops status"
|
|
1394
|
+
},
|
|
1395
|
+
steps
|
|
1396
|
+
});
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
emit({
|
|
1399
|
+
ok: false,
|
|
1400
|
+
stage: "bootstrap_failed",
|
|
1401
|
+
steps,
|
|
1402
|
+
warnings,
|
|
1403
|
+
error: error instanceof Error ? error.message : "unknown_error"
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async function commandUiStart(args) {
|
|
1409
|
+
const state = ensureOpsState();
|
|
1410
|
+
const supervisorUrl = supervisorUrlFromState(state);
|
|
1411
|
+
const supervisor = await ensureSupervisorAvailable(supervisorUrl, process.env);
|
|
1412
|
+
const ui = await ensureUiAvailable(args, process.env);
|
|
1413
|
+
emit({
|
|
1414
|
+
ok: true,
|
|
1415
|
+
supervisor_url: supervisorUrl,
|
|
1416
|
+
supervisor_started: supervisor.started,
|
|
1417
|
+
ui,
|
|
1418
|
+
next_steps: {
|
|
1419
|
+
reopen_web_ui: "delexec-ops ui start --open",
|
|
1420
|
+
refresh_status: "delexec-ops status"
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
async function main() {
|
|
1426
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1427
|
+
if (args.help || args.h) {
|
|
1428
|
+
usage();
|
|
1429
|
+
process.exit(0);
|
|
1430
|
+
}
|
|
1431
|
+
if (args._.length === 0) {
|
|
1432
|
+
usage();
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const [group, command] = args._;
|
|
1437
|
+
|
|
1438
|
+
if (group === "setup") {
|
|
1439
|
+
await commandSetup(args);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
if (group === "start") {
|
|
1443
|
+
await commandStart();
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (group === "status") {
|
|
1447
|
+
await commandStatus();
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (group === "bootstrap") {
|
|
1451
|
+
await commandBootstrap(args);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (group === "ui" && command === "start") {
|
|
1455
|
+
await commandUiStart(args);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (group === "mcp" && command === "spec") {
|
|
1459
|
+
await commandMcpSpec();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (group === "enable-responder") {
|
|
1463
|
+
await commandEnableResponder(args);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (group === "add-hotline") {
|
|
1467
|
+
await commandAddHotline(args);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (group === "attach-project") {
|
|
1471
|
+
await commandAttachProject(args);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (group === "add-example-hotline") {
|
|
1475
|
+
await commandAddExampleHotline();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (group === "enable-hotline") {
|
|
1479
|
+
await commandSetHotlineEnabled(args, true);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (group === "remove-hotline") {
|
|
1483
|
+
await commandRemoveHotline(args);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (group === "disable-hotline") {
|
|
1487
|
+
await commandSetHotlineEnabled(args, false);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (group === "doctor") {
|
|
1491
|
+
await commandDoctor();
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (group === "debug-snapshot") {
|
|
1495
|
+
await commandDebugSnapshot();
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (group === "submit-review") {
|
|
1499
|
+
await commandSubmitReview(args);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (group === "run-example") {
|
|
1503
|
+
await commandRunExample(args);
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (group === "auth" && command === "register") {
|
|
1507
|
+
await commandAuthRegister(args);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if ((group === "responder" || group === "responder") && command === "init") {
|
|
1512
|
+
await commandSetup(args);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if ((group === "responder" || group === "responder") && command === "register") {
|
|
1516
|
+
await commandSubmitReview(args);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if ((group === "responder" || group === "responder") && command === "show-draft") {
|
|
1520
|
+
await commandShowDraft(args);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if ((group === "responder" || group === "responder") && command === "submit-draft") {
|
|
1524
|
+
await commandSubmitReview(args);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if ((group === "responder" || group === "responder") && command === "add-hotline") {
|
|
1528
|
+
await commandAddHotline(args);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if ((group === "responder" || group === "responder") && command === "attach-project") {
|
|
1532
|
+
await commandAttachProject(args);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if ((group === "responder" || group === "responder") && command === "enable-hotline") {
|
|
1536
|
+
await commandSetHotlineEnabled(args, true);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if ((group === "responder" || group === "responder") && command === "remove-hotline") {
|
|
1540
|
+
await commandRemoveHotline(args);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if ((group === "responder" || group === "responder") && command === "disable-hotline") {
|
|
1544
|
+
await commandSetHotlineEnabled(args, false);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if ((group === "responder" || group === "responder") && command === "start") {
|
|
1548
|
+
await commandStart();
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if ((group === "responder" || group === "responder") && command === "status") {
|
|
1552
|
+
await commandStatus();
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if ((group === "responder" || group === "responder") && command === "doctor") {
|
|
1556
|
+
await commandDoctor();
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if ((group === "responder" || group === "responder") && command === "debug-snapshot") {
|
|
1560
|
+
await commandDebugSnapshot();
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
usage();
|
|
1565
|
+
throw new Error(`unsupported_command:${group || ""}:${command || ""}`);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
main().catch((error) => {
|
|
1569
|
+
console.error(`[delexec-ops] ${error instanceof Error ? error.message : "unknown_error"}`);
|
|
1570
|
+
process.exit(1);
|
|
1571
|
+
});
|