conductor-oss 0.2.18 → 0.2.20
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/dist/commands/dashboard.js +2 -2
- package/dist/commands/start.d.ts +3 -6
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +450 -220
- package/dist/commands/start.js.map +1 -1
- package/dist/index.js +0 -4
- package/dist/index.js.map +1 -1
- package/native/conductor +0 -0
- package/node_modules/@conductor-oss/core/dist/session-manager.js +1 -1
- package/node_modules/@conductor-oss/core/dist/session-manager.js.map +1 -1
- package/node_modules/@conductor-oss/plugin-agent-amp/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-ccr/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-claude-code/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-codex/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-cursor-cli/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-droid/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-gemini/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-github-copilot/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-opencode/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-qwen-code/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-mcp-server/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-notifier-desktop/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-notifier-discord/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-runtime-tmux/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-scm-github/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-terminal-web/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-tracker-github/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-workspace-worktree/package.json +1 -1
- package/package.json +21 -22
- package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/packages/web/.next/app-path-routes-manifest.json +0 -1
- package/web/.next/standalone/packages/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/packages/web/.next/routes-manifest.json +0 -6
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feed/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js +4 -4
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/index.rsc +4 -4
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app-paths-manifest.json +0 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1ed2e6c1._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3d6b30a3._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a0b6570d._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0e4dc4f7.js +2 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__12eb9005._.js → [root-of-the-server]__48817f02._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__6622b514._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__869d9ac0._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__9dc23e5a._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b6d31783._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_38e7b35d._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_f665760b._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_f2ebd7a9._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_79316445._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_a078c137._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/packages_web_src_app_page_tsx_cd282e82._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{packages_web_src_components_3809c507._.js → packages_web_src_components_6cab1c8c._.js} +1 -1
- package/web/.next/standalone/packages/web/.next/server/functions-config-manifest.json +1 -2
- package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
- package/web/.next/standalone/packages/web/.next/static/chunks/062888342200567f.js +1 -0
- package/web/.next/{static/chunks/28dd6ef2af62b509.js → standalone/packages/web/.next/static/chunks/3b757dce050133c8.js} +2 -2
- package/web/.next/standalone/packages/web/.next/static/chunks/{c959976264f14eba.js → 5c7e8425945ad682.js} +1 -1
- package/web/.next/standalone/packages/web/.next/static/chunks/6c9a11faed9daf4d.js +1 -0
- package/web/.next/standalone/packages/web/.next/static/chunks/8221b78965a50858.js +1 -0
- package/web/.next/standalone/packages/web/.next/static/chunks/8de84e208e201d72.css +3 -0
- package/web/.next/standalone/packages/web/.next/static/chunks/{e862e73b22fe29c2.js → c4ea57fb949fb623.js} +1 -1
- package/web/.next/standalone/packages/web/src/components/layout/TopBar.tsx +8 -10
- package/web/.next/standalone/packages/web/src/features/dashboard/DashboardClient.tsx +32 -16
- package/web/.next/standalone/packages/web/src/features/sessions/SessionPageClient.tsx +12 -2
- package/web/.next/standalone/packages/web/src/proxy.ts +1 -0
- package/web/.next/static/chunks/062888342200567f.js +1 -0
- package/web/.next/{standalone/packages/web/.next/static/chunks/28dd6ef2af62b509.js → static/chunks/3b757dce050133c8.js} +2 -2
- package/web/.next/static/chunks/{c959976264f14eba.js → 5c7e8425945ad682.js} +1 -1
- package/web/.next/static/chunks/6c9a11faed9daf4d.js +1 -0
- package/web/.next/static/chunks/8221b78965a50858.js +1 -0
- package/web/.next/static/chunks/8de84e208e201d72.css +3 -0
- package/web/.next/static/chunks/{e862e73b22fe29c2.js → c4ea57fb949fb623.js} +1 -1
- package/dist/commands/watch.d.ts +0 -12
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -84
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/webhook.d.ts +0 -12
- package/dist/commands/webhook.d.ts.map +0 -1
- package/dist/commands/webhook.js +0 -59
- package/dist/commands/webhook.js.map +0 -1
- package/node_modules/@conductor-oss/plugin-webhook/dist/index.d.ts +0 -28
- package/node_modules/@conductor-oss/plugin-webhook/dist/index.d.ts.map +0 -1
- package/node_modules/@conductor-oss/plugin-webhook/dist/index.js +0 -295
- package/node_modules/@conductor-oss/plugin-webhook/dist/index.js.map +0 -1
- package/node_modules/@conductor-oss/plugin-webhook/package.json +0 -11
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/app-paths-manifest.json +0 -3
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/build-manifest.json +0 -11
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route/server-reference-manifest.json +0 -4
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js +0 -8
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.map +0 -5
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +0 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route_client-reference-manifest.js +0 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7633d324._.js +0 -4
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bf8faac8._.js +0 -4
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_agents_route_actions_29063d1a.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_78af9fdf._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_c4bad84a._.js +0 -3
- package/web/.next/standalone/packages/web/.next/static/chunks/1e67fbc3874d3f51.js +0 -1
- package/web/.next/standalone/packages/web/.next/static/chunks/2b2a24dff50e7dc9.js +0 -1
- package/web/.next/standalone/packages/web/.next/static/chunks/483eb2824f5282c7.js +0 -1
- package/web/.next/standalone/packages/web/.next/static/chunks/860d84e1f09476a4.css +0 -3
- package/web/.next/standalone/packages/web/src/app/api/agents/route.ts +0 -223
- package/web/.next/static/chunks/1e67fbc3874d3f51.js +0 -1
- package/web/.next/static/chunks/2b2a24dff50e7dc9.js +0 -1
- package/web/.next/static/chunks/483eb2824f5282c7.js +0 -1
- package/web/.next/static/chunks/860d84e1f09476a4.css +0 -3
- /package/web/.next/standalone/packages/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_buildManifest.js +0 -0
- /package/web/.next/standalone/packages/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/packages/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_ssgManifest.js +0 -0
- /package/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_buildManifest.js +0 -0
- /package/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{eSF3qxz6RT8UXiwr-uibJ → P8IBAo_woCzrGXJiYCZto}/_ssgManifest.js +0 -0
package/dist/commands/start.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `co start`
|
|
3
3
|
*
|
|
4
|
-
* Starts the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* The lifecycle manager polls sessions periodically, advancing their
|
|
8
|
-
* state machine (checking CI, reviews, merging, sending reactions, etc.).
|
|
9
|
-
* The web dashboard provides a browser UI for monitoring and interaction.
|
|
4
|
+
* Starts the Rust backend and web dashboard in the foreground.
|
|
5
|
+
* The JS launcher is intentionally thin: it resolves paths, launches
|
|
6
|
+
* the Rust backend, and wires the frontend to it.
|
|
10
7
|
*/
|
|
11
8
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
9
|
import { randomBytes } from "node:crypto";
|
|
13
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
11
|
import { homedir } from "node:os";
|
|
15
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
16
13
|
import chalk from "chalk";
|
|
17
14
|
import ora from "ora";
|
|
15
|
+
import { parse as parseYaml } from "yaml";
|
|
18
16
|
import { buildConductorBoard, buildConductorYaml } from "@conductor-oss/core";
|
|
19
|
-
import { createServices, loadConfig } from "../services.js";
|
|
20
17
|
function commandExists(command) {
|
|
21
18
|
const checker = process.platform === "win32" ? "where" : "which";
|
|
22
19
|
const result = spawnSync(checker, [command], { stdio: "ignore" });
|
|
@@ -46,14 +43,43 @@ function resolveBuiltinRemoteAuth(enabled) {
|
|
|
46
43
|
sessionSecret,
|
|
47
44
|
};
|
|
48
45
|
}
|
|
46
|
+
function asObject(value) {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function asTrimmedString(value) {
|
|
53
|
+
if (typeof value !== "string")
|
|
54
|
+
return null;
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
57
|
+
}
|
|
58
|
+
function asBoolean(value) {
|
|
59
|
+
return value === true;
|
|
60
|
+
}
|
|
61
|
+
function coercePort(value, fallback) {
|
|
62
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
const parsed = parseInt(value.trim(), 10);
|
|
67
|
+
if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
49
73
|
function resolveTrustedHeaderAuth(config) {
|
|
74
|
+
const access = asObject(config["access"]);
|
|
75
|
+
const trustedHeaders = asObject(access["trustedHeaders"]);
|
|
50
76
|
return {
|
|
51
|
-
enabled:
|
|
52
|
-
provider:
|
|
53
|
-
emailHeader:
|
|
54
|
-
jwtHeader:
|
|
55
|
-
teamDomain:
|
|
56
|
-
audience:
|
|
77
|
+
enabled: asBoolean(trustedHeaders["enabled"]),
|
|
78
|
+
provider: trustedHeaders["provider"] === "generic" ? "generic" : "cloudflare-access",
|
|
79
|
+
emailHeader: asTrimmedString(trustedHeaders["emailHeader"]) || "Cf-Access-Authenticated-User-Email",
|
|
80
|
+
jwtHeader: asTrimmedString(trustedHeaders["jwtHeader"]) || "Cf-Access-Jwt-Assertion",
|
|
81
|
+
teamDomain: asTrimmedString(trustedHeaders["teamDomain"]),
|
|
82
|
+
audience: asTrimmedString(trustedHeaders["audience"]),
|
|
57
83
|
};
|
|
58
84
|
}
|
|
59
85
|
export function extractCloudflareTunnelUrl(output) {
|
|
@@ -162,13 +188,40 @@ async function waitForDashboard(url, timeoutMs = 15_000) {
|
|
|
162
188
|
catch {
|
|
163
189
|
// Dashboard is still starting.
|
|
164
190
|
}
|
|
165
|
-
await new Promise((
|
|
191
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
|
|
166
192
|
}
|
|
167
193
|
return false;
|
|
168
194
|
}
|
|
169
195
|
function getDefaultLauncherWorkspace() {
|
|
170
196
|
return resolve(homedir(), ".openclaw", "workspace");
|
|
171
197
|
}
|
|
198
|
+
function resolveWorkspacePathHint(configHint) {
|
|
199
|
+
if (!configHint) {
|
|
200
|
+
return getDefaultLauncherWorkspace();
|
|
201
|
+
}
|
|
202
|
+
const resolved = resolve(configHint);
|
|
203
|
+
if (existsSync(resolved) && basename(resolved).match(/^conductor\.ya?ml$/i)) {
|
|
204
|
+
return dirname(resolved);
|
|
205
|
+
}
|
|
206
|
+
return resolved;
|
|
207
|
+
}
|
|
208
|
+
function findConfigFile(startDir) {
|
|
209
|
+
const baseDir = resolveWorkspacePathHint(startDir);
|
|
210
|
+
let currentDir = baseDir;
|
|
211
|
+
for (;;) {
|
|
212
|
+
for (const filename of ["conductor.yaml", "conductor.yml"]) {
|
|
213
|
+
const candidate = join(currentDir, filename);
|
|
214
|
+
if (existsSync(candidate)) {
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const parentDir = dirname(currentDir);
|
|
219
|
+
if (parentDir === currentDir) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
currentDir = parentDir;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
172
225
|
function ensureDashboardBootstrapWorkspace() {
|
|
173
226
|
const workspacePath = getDefaultLauncherWorkspace();
|
|
174
227
|
const configPath = join(workspacePath, "conductor.yaml");
|
|
@@ -190,55 +243,279 @@ function ensureDashboardBootstrapWorkspace() {
|
|
|
190
243
|
}
|
|
191
244
|
return { workspacePath, configPath };
|
|
192
245
|
}
|
|
246
|
+
function loadLauncherSettings(configHint) {
|
|
247
|
+
const explicitConfigPath = configHint && basename(configHint).match(/^conductor\.ya?ml$/i)
|
|
248
|
+
? resolve(configHint)
|
|
249
|
+
: null;
|
|
250
|
+
const configPath = explicitConfigPath ?? findConfigFile(configHint ?? undefined);
|
|
251
|
+
if (!configPath) {
|
|
252
|
+
const bootstrap = ensureDashboardBootstrapWorkspace();
|
|
253
|
+
return {
|
|
254
|
+
workspacePath: bootstrap.workspacePath,
|
|
255
|
+
configPath: bootstrap.configPath,
|
|
256
|
+
dashboardPort: 4747,
|
|
257
|
+
backendPort: 4748,
|
|
258
|
+
access: {
|
|
259
|
+
requireAuth: false,
|
|
260
|
+
defaultRole: "operator",
|
|
261
|
+
trustedHeaders: resolveTrustedHeaderAuth({}),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const parsed = parseYaml(readFileSync(configPath, "utf8"));
|
|
266
|
+
const config = asObject(parsed);
|
|
267
|
+
const access = asObject(config["access"]);
|
|
268
|
+
const server = asObject(config["server"]);
|
|
269
|
+
return {
|
|
270
|
+
workspacePath: dirname(configPath),
|
|
271
|
+
configPath,
|
|
272
|
+
dashboardPort: 4747,
|
|
273
|
+
backendPort: coercePort(server["port"], 4748),
|
|
274
|
+
access: {
|
|
275
|
+
requireAuth: asBoolean(access["requireAuth"]),
|
|
276
|
+
defaultRole: asTrimmedString(access["defaultRole"]),
|
|
277
|
+
trustedHeaders: resolveTrustedHeaderAuth(config),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function resolveBackendPort(cliValue, configuredPort) {
|
|
282
|
+
const raw = cliValue?.trim() || process.env["CONDUCTOR_BACKEND_PORT"]?.trim();
|
|
283
|
+
if (!raw)
|
|
284
|
+
return configuredPort;
|
|
285
|
+
return coercePort(raw, configuredPort);
|
|
286
|
+
}
|
|
287
|
+
function resolveFrontendPort(cliValue, configuredPort) {
|
|
288
|
+
const raw = cliValue?.trim() || process.env["PORT"]?.trim();
|
|
289
|
+
if (!raw)
|
|
290
|
+
return configuredPort;
|
|
291
|
+
return coercePort(raw, configuredPort);
|
|
292
|
+
}
|
|
293
|
+
function resolveRepoCargoRoot(workspacePath) {
|
|
294
|
+
const candidate = resolve(workspacePath);
|
|
295
|
+
if (existsSync(join(candidate, "Cargo.toml"))
|
|
296
|
+
&& existsSync(join(candidate, "crates", "conductor-cli", "Cargo.toml"))) {
|
|
297
|
+
return candidate;
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
function resolveBundledRustBinary() {
|
|
302
|
+
const binaryName = process.platform === "win32" ? "conductor.exe" : "conductor";
|
|
303
|
+
const cliDir = new URL(".", import.meta.url).pathname;
|
|
304
|
+
const candidates = [
|
|
305
|
+
resolve(cliDir, "..", "..", "native", binaryName),
|
|
306
|
+
resolve(cliDir, "..", "..", "..", "native", binaryName),
|
|
307
|
+
];
|
|
308
|
+
for (const candidate of candidates) {
|
|
309
|
+
if (existsSync(candidate)) {
|
|
310
|
+
return candidate;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function detectNativeBinaryFormat(binaryPath) {
|
|
316
|
+
try {
|
|
317
|
+
const header = readFileSync(binaryPath).subarray(0, 4);
|
|
318
|
+
if (header.length >= 2 && header[0] === 0x4d && header[1] === 0x5a) {
|
|
319
|
+
return "pe";
|
|
320
|
+
}
|
|
321
|
+
if (header.length >= 4 && header[0] === 0x7f && header[1] === 0x45 && header[2] === 0x4c && header[3] === 0x46) {
|
|
322
|
+
return "elf";
|
|
323
|
+
}
|
|
324
|
+
if (header.length >= 4) {
|
|
325
|
+
const magic = header.readUInt32BE(0);
|
|
326
|
+
if (magic === 0xfeedface
|
|
327
|
+
|| magic === 0xcefaedfe
|
|
328
|
+
|| magic === 0xfeedfacf
|
|
329
|
+
|| magic === 0xcffaedfe
|
|
330
|
+
|| magic === 0xcafebabe
|
|
331
|
+
|| magic === 0xbebafeca
|
|
332
|
+
|| magic === 0xcafebabf) {
|
|
333
|
+
return "macho";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// ignore and treat as unknown
|
|
339
|
+
}
|
|
340
|
+
return "unknown";
|
|
341
|
+
}
|
|
342
|
+
function isCompatibleNativeBinary(binaryPath) {
|
|
343
|
+
const format = detectNativeBinaryFormat(binaryPath);
|
|
344
|
+
if (process.platform === "darwin")
|
|
345
|
+
return format === "macho";
|
|
346
|
+
if (process.platform === "linux")
|
|
347
|
+
return format === "elf";
|
|
348
|
+
if (process.platform === "win32")
|
|
349
|
+
return format === "pe";
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
function describeNativeBinaryHostMismatch(binaryPath) {
|
|
353
|
+
const format = detectNativeBinaryFormat(binaryPath);
|
|
354
|
+
return `Bundled Rust backend is incompatible with ${process.platform}-${process.arch} (binary format: ${format}).`;
|
|
355
|
+
}
|
|
356
|
+
function resolveRustBackendLaunch(workspacePath, configPath, backendPort) {
|
|
357
|
+
const bundledBinary = resolveBundledRustBinary();
|
|
358
|
+
if (bundledBinary) {
|
|
359
|
+
if (!isCompatibleNativeBinary(bundledBinary)) {
|
|
360
|
+
return {
|
|
361
|
+
launch: null,
|
|
362
|
+
reason: describeNativeBinaryHostMismatch(bundledBinary),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
launch: {
|
|
367
|
+
cmd: bundledBinary,
|
|
368
|
+
args: [
|
|
369
|
+
"--workspace",
|
|
370
|
+
workspacePath,
|
|
371
|
+
"--config",
|
|
372
|
+
configPath,
|
|
373
|
+
"start",
|
|
374
|
+
"--host",
|
|
375
|
+
"127.0.0.1",
|
|
376
|
+
"--port",
|
|
377
|
+
String(backendPort),
|
|
378
|
+
],
|
|
379
|
+
cwd: workspacePath,
|
|
380
|
+
label: "bundled Rust backend",
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const repoCargoRoot = resolveRepoCargoRoot(workspacePath);
|
|
385
|
+
if (!repoCargoRoot) {
|
|
386
|
+
return {
|
|
387
|
+
launch: null,
|
|
388
|
+
reason: "No compatible bundled Rust backend was found, and this install does not have a repo-local Cargo fallback.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const binaryName = process.platform === "win32" ? "conductor.exe" : "conductor";
|
|
392
|
+
const prebuiltCandidates = [
|
|
393
|
+
join(repoCargoRoot, "target", "release", binaryName),
|
|
394
|
+
join(repoCargoRoot, "target", "debug", binaryName),
|
|
395
|
+
];
|
|
396
|
+
for (const candidate of prebuiltCandidates) {
|
|
397
|
+
if (existsSync(candidate)) {
|
|
398
|
+
return {
|
|
399
|
+
launch: {
|
|
400
|
+
cmd: candidate,
|
|
401
|
+
args: [
|
|
402
|
+
"--workspace",
|
|
403
|
+
workspacePath,
|
|
404
|
+
"--config",
|
|
405
|
+
configPath,
|
|
406
|
+
"start",
|
|
407
|
+
"--host",
|
|
408
|
+
"127.0.0.1",
|
|
409
|
+
"--port",
|
|
410
|
+
String(backendPort),
|
|
411
|
+
],
|
|
412
|
+
cwd: repoCargoRoot,
|
|
413
|
+
label: "prebuilt Rust backend",
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
launch: {
|
|
420
|
+
cmd: "cargo",
|
|
421
|
+
args: [
|
|
422
|
+
"run",
|
|
423
|
+
"-p",
|
|
424
|
+
"conductor-cli",
|
|
425
|
+
"--",
|
|
426
|
+
"--workspace",
|
|
427
|
+
workspacePath,
|
|
428
|
+
"--config",
|
|
429
|
+
configPath,
|
|
430
|
+
"start",
|
|
431
|
+
"--host",
|
|
432
|
+
"127.0.0.1",
|
|
433
|
+
"--port",
|
|
434
|
+
String(backendPort),
|
|
435
|
+
],
|
|
436
|
+
cwd: repoCargoRoot,
|
|
437
|
+
label: "cargo-run Rust backend",
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async function waitForHttpService(url, timeoutMs = 15_000) {
|
|
442
|
+
const startedAt = Date.now();
|
|
443
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
444
|
+
try {
|
|
445
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
446
|
+
if (response.ok || response.status < 500) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
// Service is still starting.
|
|
452
|
+
}
|
|
453
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
async function killStalePortListener(port) {
|
|
458
|
+
try {
|
|
459
|
+
const { execSync } = await import("node:child_process");
|
|
460
|
+
const pids = execSync(`lsof -ti :${port} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf8" }).trim();
|
|
461
|
+
if (!pids) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (const pid of pids.split("\n").filter(Boolean)) {
|
|
465
|
+
if (pid !== String(process.pid)) {
|
|
466
|
+
process.kill(Number(pid), "SIGTERM");
|
|
467
|
+
console.log(`[port] Killed stale process ${pid} on port ${port}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
// no stale process — expected
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function resolveDashboardWebMode(mode) {
|
|
477
|
+
switch (mode?.trim().toLowerCase()) {
|
|
478
|
+
case "dev":
|
|
479
|
+
return "dev";
|
|
480
|
+
case "production":
|
|
481
|
+
case "prod":
|
|
482
|
+
return "production";
|
|
483
|
+
case "standalone":
|
|
484
|
+
return "standalone";
|
|
485
|
+
default:
|
|
486
|
+
return "auto";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
193
489
|
export function registerStart(program) {
|
|
194
490
|
program
|
|
195
491
|
.command("start")
|
|
196
|
-
.description("Start
|
|
492
|
+
.description("Start the Rust backend and web dashboard (foreground)")
|
|
197
493
|
.option("--no-dashboard", "Skip starting the web dashboard")
|
|
198
|
-
.option("--no-watcher", "
|
|
494
|
+
.option("--no-watcher", "Deprecated. Rust backend startup no longer uses the JS watcher")
|
|
199
495
|
.option("--open", "Open the dashboard in your default browser")
|
|
200
496
|
.option("--tunnel", "Expose the dashboard on a free public Cloudflare Quick Tunnel")
|
|
201
497
|
.option("--host <host>", "Dashboard bind host. Defaults to 127.0.0.1 for local-only access")
|
|
202
498
|
.option("-p, --port <port>", "Dashboard port override")
|
|
203
|
-
.option("-
|
|
499
|
+
.option("--no-backend", "Do not launch a separate local Rust backend")
|
|
500
|
+
.option("--backend-port <port>", "Rust backend port override (default: from config or 4748)")
|
|
501
|
+
.option("-w, --workspace <path>", "Workspace path or conductor.yaml path")
|
|
204
502
|
.action(async (opts) => {
|
|
205
503
|
try {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!/No conductor\.ya?ml found/i.test(message)) {
|
|
218
|
-
throw err;
|
|
219
|
-
}
|
|
220
|
-
const bootstrap = ensureDashboardBootstrapWorkspace();
|
|
221
|
-
process.env["CONDUCTOR_WORKSPACE"] = bootstrap.workspacePath;
|
|
222
|
-
process.env["CO_CONFIG_PATH"] = bootstrap.configPath;
|
|
223
|
-
config = await loadConfig(bootstrap.workspacePath);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const workspacePath = opts.workspace
|
|
227
|
-
?? process.env["CONDUCTOR_WORKSPACE"]
|
|
228
|
-
?? (config.configPath ? dirname(config.configPath) : `${process.env["HOME"]}/.conductor/workspace`);
|
|
229
|
-
if (!process.env["CONDUCTOR_WORKSPACE"]) {
|
|
230
|
-
process.env["CONDUCTOR_WORKSPACE"] = workspacePath;
|
|
231
|
-
}
|
|
232
|
-
if (!process.env["CO_CONFIG_PATH"] && config.configPath) {
|
|
233
|
-
process.env["CO_CONFIG_PATH"] = config.configPath;
|
|
234
|
-
}
|
|
235
|
-
const { sessionManager, registry } = await createServices(config);
|
|
236
|
-
const supportedAgents = registry.list("agent").map((agent) => agent.name);
|
|
237
|
-
// Mutable ref — set after boardWatcher is created, used by lifecycle callback
|
|
238
|
-
let boardWatcherRef = null;
|
|
239
|
-
const port = opts.port ? parseInt(opts.port, 10) : (config.port ?? 3000);
|
|
504
|
+
const configHint = opts.workspace
|
|
505
|
+
|| process.env["CO_CONFIG_PATH"]?.trim()
|
|
506
|
+
|| process.env["CONDUCTOR_WORKSPACE"]
|
|
507
|
+
|| null;
|
|
508
|
+
const settings = loadLauncherSettings(configHint);
|
|
509
|
+
const workspacePath = settings.workspacePath;
|
|
510
|
+
const configPath = settings.configPath;
|
|
511
|
+
const dashboardPort = resolveFrontendPort(opts.port, settings.dashboardPort);
|
|
512
|
+
const backendPort = resolveBackendPort(opts.backendPort, settings.backendPort);
|
|
513
|
+
const explicitBackendUrl = process.env["CONDUCTOR_BACKEND_URL"]?.trim() || null;
|
|
514
|
+
const bindHost = opts.host?.trim() || "127.0.0.1";
|
|
240
515
|
const shutdownTasks = [];
|
|
241
516
|
let isShuttingDown = false;
|
|
517
|
+
process.env["CONDUCTOR_WORKSPACE"] = workspacePath;
|
|
518
|
+
process.env["CO_CONFIG_PATH"] = configPath;
|
|
242
519
|
const requestShutdown = () => {
|
|
243
520
|
void (async () => {
|
|
244
521
|
if (isShuttingDown)
|
|
@@ -248,8 +525,8 @@ export function registerStart(program) {
|
|
|
248
525
|
try {
|
|
249
526
|
await task();
|
|
250
527
|
}
|
|
251
|
-
catch (
|
|
252
|
-
console.error(
|
|
528
|
+
catch (error) {
|
|
529
|
+
console.error(error);
|
|
253
530
|
}
|
|
254
531
|
}
|
|
255
532
|
process.exit(0);
|
|
@@ -262,100 +539,82 @@ export function registerStart(program) {
|
|
|
262
539
|
console.log(chalk.bold.cyan(" Conductor -- Starting"));
|
|
263
540
|
console.log(chalk.dim(line));
|
|
264
541
|
console.log();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const core = await import("@conductor-oss/core");
|
|
268
|
-
core.syncWorkspaceSupportFiles(config, {
|
|
269
|
-
workspacePath,
|
|
270
|
-
agentNames: supportedAgents,
|
|
271
|
-
});
|
|
272
|
-
// Startup config sync: regenerate drifted project-local conductor.yaml mirrors
|
|
273
|
-
if (typeof core.startupConfigSync === "function") {
|
|
274
|
-
const syncResult = core.startupConfigSync(config);
|
|
275
|
-
if (syncResult.fixed > 0) {
|
|
276
|
-
console.log(chalk.dim(` Synced ${syncResult.fixed} project-local config(s) from canonical`));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (typeof core.createLifecycleManager !== "function") {
|
|
280
|
-
spinner.warn("Lifecycle manager not yet implemented in @conductor-oss/core");
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
const lifecycle = core.createLifecycleManager({
|
|
284
|
-
config,
|
|
285
|
-
sessionManager,
|
|
286
|
-
onStatusChange: (sessionId, newStatus, projectId) => {
|
|
287
|
-
console.log(`[lifecycle] Status change: ${sessionId} → ${newStatus} (${projectId})`);
|
|
288
|
-
// Trigger immediate board sync
|
|
289
|
-
boardWatcherRef?.updateNow();
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
lifecycle.start(10_000); // Poll every 10s (was 30s)
|
|
293
|
-
spinner.succeed("Lifecycle manager running");
|
|
294
|
-
// Graceful shutdown
|
|
295
|
-
shutdownTasks.push(() => {
|
|
296
|
-
console.log(chalk.dim("\nShutting down lifecycle manager..."));
|
|
297
|
-
lifecycle.stop();
|
|
298
|
-
});
|
|
542
|
+
if (opts.watcher === false) {
|
|
543
|
+
console.log(chalk.dim(" JS watcher flag ignored: runtime ownership has moved to the Rust backend path."));
|
|
299
544
|
}
|
|
300
|
-
// ---- Start
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
545
|
+
// ---- Start Rust backend ----
|
|
546
|
+
let backendProcess = null;
|
|
547
|
+
const shouldLaunchBackend = opts.backend !== false && !explicitBackendUrl;
|
|
548
|
+
const backendUrl = explicitBackendUrl ?? (shouldLaunchBackend ? `http://127.0.0.1:${backendPort}` : null);
|
|
549
|
+
if (shouldLaunchBackend) {
|
|
550
|
+
const backendSpinner = ora(`Starting Rust backend on http://127.0.0.1:${backendPort}`).start();
|
|
551
|
+
const resolution = resolveRustBackendLaunch(workspacePath, configPath, backendPort);
|
|
552
|
+
const launch = resolution.launch;
|
|
553
|
+
if (!launch) {
|
|
554
|
+
throw new Error(resolution.reason ?? "Rust backend binary was not found. Build or package the Rust backend first.");
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
try {
|
|
558
|
+
await killStalePortListener(backendPort);
|
|
559
|
+
let backendStartError = null;
|
|
560
|
+
backendProcess = spawn(launch.cmd, launch.args, {
|
|
561
|
+
cwd: launch.cwd,
|
|
562
|
+
stdio: "inherit",
|
|
563
|
+
detached: false,
|
|
564
|
+
env: {
|
|
565
|
+
...process.env,
|
|
321
566
|
},
|
|
322
567
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
568
|
+
backendProcess.once("error", (error) => {
|
|
569
|
+
backendStartError = error;
|
|
570
|
+
});
|
|
571
|
+
const backendReady = await waitForHttpService(`${backendUrl}/api/health`);
|
|
572
|
+
if (!backendReady) {
|
|
573
|
+
const reason = (backendStartError ? String(backendStartError) : null)
|
|
574
|
+
|| (backendProcess.exitCode !== null
|
|
575
|
+
? `Rust backend exited with code ${backendProcess.exitCode}`
|
|
576
|
+
: `Rust backend did not become ready at ${backendUrl} in time.`);
|
|
577
|
+
throw new Error(reason);
|
|
578
|
+
}
|
|
579
|
+
backendSpinner.succeed(`Rust backend running on ${backendUrl} (${launch.label})`);
|
|
580
|
+
shutdownTasks.push(() => {
|
|
581
|
+
if (backendProcess && backendProcess.exitCode === null) {
|
|
582
|
+
backendProcess.kill("SIGTERM");
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
backendSpinner.fail(`Rust backend failed: ${error}`);
|
|
588
|
+
throw error;
|
|
327
589
|
}
|
|
328
590
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
591
|
+
}
|
|
592
|
+
else if (explicitBackendUrl) {
|
|
593
|
+
console.log(chalk.dim(` Backend: using existing Rust backend at ${explicitBackendUrl}`));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
console.log(chalk.yellow(" Backend: not launched; frontend API requests will fail without CONDUCTOR_BACKEND_URL."));
|
|
332
597
|
}
|
|
333
598
|
// ---- Start web dashboard ----
|
|
334
599
|
let dashboardProcess = null;
|
|
335
600
|
let publicDashboardUrl = null;
|
|
336
601
|
let unlockDashboardUrl = null;
|
|
337
|
-
const bindHost = opts.host?.trim() || "127.0.0.1";
|
|
338
602
|
const externalAccessRequested = opts.tunnel === true || !isLoopbackHost(bindHost);
|
|
339
603
|
const clerkConfigured = Boolean(process.env["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"] && process.env["CLERK_SECRET_KEY"]);
|
|
340
|
-
const trustedHeaderAuth =
|
|
604
|
+
const trustedHeaderAuth = settings.access.trustedHeaders;
|
|
341
605
|
const builtinRemoteAuth = resolveBuiltinRemoteAuth(externalAccessRequested && !clerkConfigured);
|
|
342
606
|
if (opts.dashboard !== false) {
|
|
343
607
|
const dashSpinner = ora("Starting web dashboard").start();
|
|
344
608
|
try {
|
|
345
609
|
const cliDir = new URL(".", import.meta.url).pathname;
|
|
346
|
-
const {
|
|
347
|
-
const { cpSync, existsSync, mkdirSync } = await import("node:fs");
|
|
348
|
-
// Search order:
|
|
349
|
-
// 1. Standalone build inside CLI package (npm install -g)
|
|
350
|
-
// 2. Sibling packages/web in monorepo dev setup
|
|
351
|
-
// 3. config.configPath-relative monorepo root
|
|
610
|
+
const { cpSync, readdirSync, statSync } = await import("node:fs");
|
|
352
611
|
let webDir = null;
|
|
353
612
|
const candidates = [
|
|
354
|
-
resolve(cliDir, "..", "web"),
|
|
355
|
-
resolve(cliDir, "..", "..", "..", "web"),
|
|
356
|
-
resolve(cliDir, "..", "..", "web"),
|
|
357
|
-
|
|
358
|
-
]
|
|
613
|
+
resolve(cliDir, "..", "web"),
|
|
614
|
+
resolve(cliDir, "..", "..", "..", "web"),
|
|
615
|
+
resolve(cliDir, "..", "..", "web"),
|
|
616
|
+
resolve(dirname(configPath), "packages", "web"),
|
|
617
|
+
];
|
|
359
618
|
for (const candidate of candidates) {
|
|
360
619
|
if (existsSync(join(candidate, "package.json"))) {
|
|
361
620
|
webDir = candidate;
|
|
@@ -366,52 +625,49 @@ export function registerStart(program) {
|
|
|
366
625
|
dashSpinner.warn("Dashboard not found. Run: pnpm --filter @conductor-oss/web build");
|
|
367
626
|
return;
|
|
368
627
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
for (const pid of pids.split("\n").filter(Boolean)) {
|
|
375
|
-
if (pid !== String(process.pid)) {
|
|
376
|
-
process.kill(Number(pid), "SIGTERM");
|
|
377
|
-
console.log(`[dashboard] Killed stale process ${pid} on port ${port}`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
catch { /* no stale process — expected */ }
|
|
384
|
-
// Prefer standalone build (output: standalone in next.config), then production, then dev
|
|
385
|
-
// Find standalone server.js (location varies by monorepo nesting)
|
|
386
|
-
const { readdirSync, statSync: fsStat } = await import("node:fs");
|
|
628
|
+
await killStalePortListener(dashboardPort);
|
|
629
|
+
const webMode = resolveDashboardWebMode(process.env["CONDUCTOR_WEB_MODE"]);
|
|
630
|
+
const isSourceCheckout = existsSync(join(webDir, "src", "app", "page.tsx"))
|
|
631
|
+
&& existsSync(join(webDir, "next.config.ts"));
|
|
632
|
+
const preferDevServer = webMode === "dev" || (webMode === "auto" && isSourceCheckout);
|
|
387
633
|
const standaloneDir = join(webDir, ".next", "standalone");
|
|
634
|
+
const hasNextBuild = existsSync(join(webDir, ".next"));
|
|
388
635
|
let standaloneServer = null;
|
|
389
636
|
const searchQueue = [standaloneDir];
|
|
390
|
-
for (let depth = 0; depth < 6 && searchQueue.length > 0 && !standaloneServer; depth
|
|
637
|
+
for (let depth = 0; depth < 6 && searchQueue.length > 0 && !standaloneServer; depth += 1) {
|
|
391
638
|
const nextQueue = [];
|
|
392
|
-
for (const
|
|
393
|
-
const candidate = join(
|
|
639
|
+
for (const currentDir of searchQueue) {
|
|
640
|
+
const candidate = join(currentDir, "server.js");
|
|
394
641
|
if (existsSync(candidate)) {
|
|
395
642
|
standaloneServer = candidate;
|
|
396
643
|
break;
|
|
397
644
|
}
|
|
398
645
|
try {
|
|
399
|
-
for (const entry of readdirSync(
|
|
400
|
-
const full = join(
|
|
401
|
-
if (
|
|
646
|
+
for (const entry of readdirSync(currentDir)) {
|
|
647
|
+
const full = join(currentDir, String(entry));
|
|
648
|
+
if (statSync(full).isDirectory() && entry !== "node_modules") {
|
|
402
649
|
nextQueue.push(full);
|
|
403
650
|
}
|
|
404
651
|
}
|
|
405
652
|
}
|
|
406
|
-
catch {
|
|
653
|
+
catch {
|
|
654
|
+
// ignore
|
|
655
|
+
}
|
|
407
656
|
}
|
|
408
657
|
searchQueue.splice(0, searchQueue.length, ...nextQueue);
|
|
409
658
|
}
|
|
410
|
-
const hasNextBuild = existsSync(join(webDir, ".next"));
|
|
411
659
|
let cmd;
|
|
412
660
|
let args;
|
|
413
661
|
let dashboardCwd = webDir;
|
|
414
|
-
if (
|
|
662
|
+
if (preferDevServer) {
|
|
663
|
+
cmd = "pnpm";
|
|
664
|
+
args = ["run", "dev", "--hostname", bindHost, "--port", String(dashboardPort)];
|
|
665
|
+
}
|
|
666
|
+
else if (webMode === "production" && hasNextBuild) {
|
|
667
|
+
cmd = "pnpm";
|
|
668
|
+
args = ["run", "start", "--hostname", bindHost, "--port", String(dashboardPort)];
|
|
669
|
+
}
|
|
670
|
+
else if (standaloneServer) {
|
|
415
671
|
const standaloneAppDir = dirname(standaloneServer);
|
|
416
672
|
const standaloneStaticDir = join(standaloneAppDir, ".next", "static");
|
|
417
673
|
const sourceStaticDir = join(webDir, ".next", "static");
|
|
@@ -429,27 +685,12 @@ export function registerStart(program) {
|
|
|
429
685
|
dashboardCwd = standaloneDir;
|
|
430
686
|
}
|
|
431
687
|
else if (hasNextBuild) {
|
|
432
|
-
// Use pnpm run start (next start) — reliable, serves static assets correctly
|
|
433
688
|
cmd = "pnpm";
|
|
434
|
-
args = [
|
|
435
|
-
"run",
|
|
436
|
-
"start",
|
|
437
|
-
"--hostname",
|
|
438
|
-
bindHost,
|
|
439
|
-
"--port",
|
|
440
|
-
String(port),
|
|
441
|
-
];
|
|
689
|
+
args = ["run", "start", "--hostname", bindHost, "--port", String(dashboardPort)];
|
|
442
690
|
}
|
|
443
691
|
else {
|
|
444
692
|
cmd = "pnpm";
|
|
445
|
-
args = [
|
|
446
|
-
"run",
|
|
447
|
-
"dev",
|
|
448
|
-
"--hostname",
|
|
449
|
-
bindHost,
|
|
450
|
-
"--port",
|
|
451
|
-
String(port),
|
|
452
|
-
];
|
|
693
|
+
args = ["run", "dev", "--hostname", bindHost, "--port", String(dashboardPort)];
|
|
453
694
|
}
|
|
454
695
|
dashboardProcess = spawn(cmd, args, {
|
|
455
696
|
cwd: dashboardCwd,
|
|
@@ -457,10 +698,16 @@ export function registerStart(program) {
|
|
|
457
698
|
detached: false,
|
|
458
699
|
env: {
|
|
459
700
|
...process.env,
|
|
460
|
-
PORT: String(
|
|
701
|
+
PORT: String(dashboardPort),
|
|
461
702
|
HOSTNAME: bindHost,
|
|
462
703
|
CONDUCTOR_WORKSPACE: workspacePath,
|
|
463
|
-
CO_CONFIG_PATH:
|
|
704
|
+
CO_CONFIG_PATH: configPath,
|
|
705
|
+
...(backendUrl
|
|
706
|
+
? {
|
|
707
|
+
CONDUCTOR_BACKEND_URL: backendUrl,
|
|
708
|
+
CONDUCTOR_BACKEND_PORT: String(backendPort),
|
|
709
|
+
}
|
|
710
|
+
: {}),
|
|
464
711
|
...(builtinRemoteAuth
|
|
465
712
|
? {
|
|
466
713
|
CONDUCTOR_REMOTE_ACCESS_TOKEN: builtinRemoteAuth.accessToken,
|
|
@@ -481,25 +728,21 @@ export function registerStart(program) {
|
|
|
481
728
|
: {}),
|
|
482
729
|
}
|
|
483
730
|
: {}),
|
|
484
|
-
...(
|
|
485
|
-
? {
|
|
486
|
-
CONDUCTOR_REQUIRE_AUTH: "true",
|
|
487
|
-
}
|
|
731
|
+
...(settings.access.requireAuth
|
|
732
|
+
? { CONDUCTOR_REQUIRE_AUTH: "true" }
|
|
488
733
|
: {}),
|
|
489
|
-
...(
|
|
490
|
-
? {
|
|
491
|
-
CONDUCTOR_ACCESS_DEFAULT_ROLE: config.access.defaultRole,
|
|
492
|
-
}
|
|
734
|
+
...(settings.access.defaultRole
|
|
735
|
+
? { CONDUCTOR_ACCESS_DEFAULT_ROLE: settings.access.defaultRole }
|
|
493
736
|
: {}),
|
|
494
737
|
},
|
|
495
738
|
});
|
|
496
739
|
dashboardProcess.on("error", () => {
|
|
497
740
|
dashSpinner.warn("Dashboard failed to start. Try: cd packages/web && pnpm build");
|
|
498
741
|
});
|
|
499
|
-
const dashboardInternalUrl = `http://127.0.0.1:${
|
|
742
|
+
const dashboardInternalUrl = `http://127.0.0.1:${dashboardPort}`;
|
|
500
743
|
const dashboardUrl = isLoopbackHost(bindHost)
|
|
501
|
-
? `http://localhost:${
|
|
502
|
-
: `http://${bindHost}:${
|
|
744
|
+
? `http://localhost:${dashboardPort}`
|
|
745
|
+
: `http://${bindHost}:${dashboardPort}`;
|
|
503
746
|
if (builtinRemoteAuth) {
|
|
504
747
|
unlockDashboardUrl = buildRemoteUnlockUrl(dashboardUrl, builtinRemoteAuth.accessToken);
|
|
505
748
|
}
|
|
@@ -518,7 +761,6 @@ export function registerStart(program) {
|
|
|
518
761
|
}
|
|
519
762
|
});
|
|
520
763
|
publicDashboardUrl = await tunnel.url;
|
|
521
|
-
config.dashboardUrl = publicDashboardUrl;
|
|
522
764
|
if (builtinRemoteAuth) {
|
|
523
765
|
unlockDashboardUrl = buildRemoteUnlockUrl(publicDashboardUrl, builtinRemoteAuth.accessToken);
|
|
524
766
|
}
|
|
@@ -546,33 +788,22 @@ export function registerStart(program) {
|
|
|
546
788
|
});
|
|
547
789
|
}
|
|
548
790
|
}
|
|
549
|
-
catch {
|
|
550
|
-
dashSpinner.warn(
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
// ---- Start webhook server (if enabled in config) ----
|
|
554
|
-
if (config.webhook?.enabled) {
|
|
555
|
-
const webhookSpinner = ora("Starting webhook server").start();
|
|
556
|
-
try {
|
|
557
|
-
const { createWebhookServer } = await import("@conductor-oss/plugin-webhook");
|
|
558
|
-
const webhookServer = createWebhookServer(config, config.webhook);
|
|
559
|
-
await webhookServer.start();
|
|
560
|
-
shutdownTasks.push(() => webhookServer.stop());
|
|
561
|
-
webhookSpinner.succeed(`Webhook server running on port ${config.webhook.port}`);
|
|
562
|
-
}
|
|
563
|
-
catch (err) {
|
|
564
|
-
webhookSpinner.warn(`Webhook server failed to start: ${err}`);
|
|
791
|
+
catch (error) {
|
|
792
|
+
dashSpinner.warn(`Could not start dashboard: ${error}`);
|
|
565
793
|
}
|
|
566
794
|
}
|
|
567
795
|
// ---- Summary ----
|
|
568
796
|
console.log();
|
|
569
797
|
console.log(chalk.bold.green("Conductor is running."));
|
|
570
|
-
console.log(chalk.dim(` Config: ${
|
|
798
|
+
console.log(chalk.dim(` Config: ${configPath}`));
|
|
571
799
|
if (opts.dashboard !== false) {
|
|
572
800
|
const dashboardSummaryUrl = isLoopbackHost(bindHost)
|
|
573
|
-
? `http://localhost:${
|
|
574
|
-
: `http://${bindHost}:${
|
|
801
|
+
? `http://localhost:${dashboardPort}`
|
|
802
|
+
: `http://${bindHost}:${dashboardPort}`;
|
|
575
803
|
console.log(chalk.dim(` Dashboard: ${dashboardSummaryUrl}`));
|
|
804
|
+
if (backendUrl) {
|
|
805
|
+
console.log(chalk.dim(` Backend: ${backendUrl}`));
|
|
806
|
+
}
|
|
576
807
|
if (publicDashboardUrl) {
|
|
577
808
|
console.log(chalk.dim(` Public: ${publicDashboardUrl}`));
|
|
578
809
|
}
|
|
@@ -594,30 +825,29 @@ export function registerStart(program) {
|
|
|
594
825
|
}
|
|
595
826
|
}
|
|
596
827
|
}
|
|
597
|
-
|
|
598
|
-
console.log(chalk.dim(" Watcher: Obsidian CONDUCTOR.md boards"));
|
|
599
|
-
}
|
|
600
|
-
if (config.webhook?.enabled) {
|
|
601
|
-
console.log(chalk.dim(` Webhook: http://localhost:${config.webhook.port}/api/webhook`));
|
|
602
|
-
}
|
|
828
|
+
console.log(chalk.dim(" Runtime: Rust backend + Next frontend"));
|
|
603
829
|
console.log(chalk.dim(" Press Ctrl-C to stop.\n"));
|
|
604
|
-
// Keep process alive. Dashboard is optional for orchestrator health.
|
|
605
|
-
// If it crashes (e.g. EADDRINUSE), keep lifecycle + board watcher running.
|
|
606
830
|
if (dashboardProcess) {
|
|
607
831
|
dashboardProcess.on("exit", (code, signal) => {
|
|
608
832
|
if (code !== 0 && code !== null) {
|
|
609
833
|
console.error(chalk.yellow(`Dashboard exited with code ${code}${signal ? ` (signal ${signal})` : ""}. ` +
|
|
610
|
-
"Keeping
|
|
834
|
+
"Keeping the Rust backend running."));
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (backendProcess) {
|
|
839
|
+
backendProcess.on("exit", (code, signal) => {
|
|
840
|
+
if (code !== 0 && code !== null) {
|
|
841
|
+
console.error(chalk.red(`Rust backend exited with code ${code}${signal ? ` (signal ${signal})` : ""}.`));
|
|
611
842
|
}
|
|
612
843
|
});
|
|
613
844
|
}
|
|
614
|
-
// Always keep process alive via interval heartbeat.
|
|
615
845
|
setInterval(() => {
|
|
616
|
-
//
|
|
846
|
+
// Keep the launcher attached while child processes run.
|
|
617
847
|
}, 60_000);
|
|
618
848
|
}
|
|
619
|
-
catch (
|
|
620
|
-
console.error(chalk.red(`Failed to start: ${
|
|
849
|
+
catch (error) {
|
|
850
|
+
console.error(chalk.red(`Failed to start: ${error}`));
|
|
621
851
|
process.exit(1);
|
|
622
852
|
}
|
|
623
853
|
});
|