@dadado/agent-kit-cli 4.8.9 → 5.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/LICENSE +75 -21
- package/README.md +19 -0
- package/dashboard/dashboard-data.mjs +103 -0
- package/dashboard/dashboard.html +539 -172
- package/dashboard/lib/broadcast-share.mjs +251 -0
- package/dashboard/lib/guards.d.mts +10 -0
- package/dashboard/lib/guards.mjs +71 -4
- package/dashboard/lib/open-browser.d.mts +31 -0
- package/dashboard/lib/open-browser.mjs +298 -0
- package/dashboard/lib/semantic-model.mjs +215 -9
- package/dashboard/open.html +213 -0
- package/dashboard/serve.mjs +6 -2
- package/dashboard/start-broadcast.mjs +75 -31
- package/dashboard/start.mjs +23 -26
- package/dist/index.js +1837 -447
- package/package.json +4 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="robots" content="noindex,nofollow" />
|
|
7
|
+
<title>Mission Control share</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark light;
|
|
11
|
+
--bg: #0f1419;
|
|
12
|
+
--fg: #e7ecf1;
|
|
13
|
+
--muted: #8b9aab;
|
|
14
|
+
--accent: #3b82f6;
|
|
15
|
+
--err: #f87171;
|
|
16
|
+
--card: #1a222c;
|
|
17
|
+
}
|
|
18
|
+
@media (prefers-color-scheme: light) {
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #f4f6f8;
|
|
21
|
+
--fg: #12202e;
|
|
22
|
+
--muted: #5b6b7c;
|
|
23
|
+
--card: #ffffff;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
}
|
|
29
|
+
body {
|
|
30
|
+
margin: 0;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
color: var(--fg);
|
|
35
|
+
display: grid;
|
|
36
|
+
place-items: center;
|
|
37
|
+
padding: 1.5rem;
|
|
38
|
+
}
|
|
39
|
+
main {
|
|
40
|
+
width: min(28rem, 100%);
|
|
41
|
+
background: var(--card);
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
padding: 1.5rem 1.35rem;
|
|
44
|
+
box-shadow: 0 12px 40px rgb(0 0 0 / 18%);
|
|
45
|
+
}
|
|
46
|
+
h1 {
|
|
47
|
+
font-size: 1.15rem;
|
|
48
|
+
margin: 0 0 0.5rem;
|
|
49
|
+
font-weight: 650;
|
|
50
|
+
}
|
|
51
|
+
p {
|
|
52
|
+
margin: 0 0 1rem;
|
|
53
|
+
color: var(--muted);
|
|
54
|
+
font-size: 0.95rem;
|
|
55
|
+
line-height: 1.45;
|
|
56
|
+
}
|
|
57
|
+
.err {
|
|
58
|
+
color: var(--err);
|
|
59
|
+
}
|
|
60
|
+
a.btn,
|
|
61
|
+
button.btn {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
width: 100%;
|
|
66
|
+
border: 0;
|
|
67
|
+
border-radius: 8px;
|
|
68
|
+
padding: 0.75rem 1rem;
|
|
69
|
+
font: inherit;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
text-decoration: none;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
background: var(--accent);
|
|
74
|
+
color: #fff;
|
|
75
|
+
}
|
|
76
|
+
a.btn[hidden],
|
|
77
|
+
button.btn[hidden] {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
.note {
|
|
81
|
+
margin-top: 1rem;
|
|
82
|
+
font-size: 0.8rem;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<main>
|
|
88
|
+
<h1>Mission Control</h1>
|
|
89
|
+
<p id="status">Opening your local Mission Control…</p>
|
|
90
|
+
<a id="open" class="btn" hidden href="#">Open Mission Control</a>
|
|
91
|
+
<p class="note">
|
|
92
|
+
Cosmetic share link only. You must be on the same trusted LAN (or VPN) as the host.
|
|
93
|
+
Treat the full Share URL as a secret (it embeds the session token in the fragment).
|
|
94
|
+
Only private/loopback LAN targets are accepted.
|
|
95
|
+
</p>
|
|
96
|
+
</main>
|
|
97
|
+
<script>
|
|
98
|
+
(function () {
|
|
99
|
+
var statusEl = document.getElementById("status");
|
|
100
|
+
var openEl = document.getElementById("open");
|
|
101
|
+
|
|
102
|
+
function fail(msg) {
|
|
103
|
+
statusEl.textContent = msg;
|
|
104
|
+
statusEl.className = "err";
|
|
105
|
+
openEl.hidden = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isPrivateOrLoopbackHostname(hostname) {
|
|
109
|
+
var host = String(hostname || "")
|
|
110
|
+
.trim()
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/^\[|\]$/g, "");
|
|
113
|
+
if (!host) return false;
|
|
114
|
+
if (host === "localhost" || host.slice(-10) === ".localhost" || host.slice(-6) === ".local") {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
|
|
118
|
+
var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
119
|
+
if (m) {
|
|
120
|
+
var a = +m[1];
|
|
121
|
+
var b = +m[2];
|
|
122
|
+
var c = +m[3];
|
|
123
|
+
var d = +m[4];
|
|
124
|
+
if ([a, b, c, d].some(function (n) {
|
|
125
|
+
return !Number.isInteger(n) || n < 0 || n > 255;
|
|
126
|
+
})) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (a === 127) return true;
|
|
130
|
+
if (a === 10) return true;
|
|
131
|
+
if (a === 192 && b === 168) return true;
|
|
132
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
133
|
+
if (a === 169 && b === 254) return true;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (host.indexOf(":") !== -1) {
|
|
137
|
+
if (host.slice(0, 2) === "fc" || host.slice(0, 2) === "fd") return true;
|
|
138
|
+
if (/^fe[89ab]/.test(host)) return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateTarget(url) {
|
|
144
|
+
var raw = typeof url === "string" ? url.trim() : "";
|
|
145
|
+
if (!raw) return { ok: false, error: "invalid-target" };
|
|
146
|
+
var parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = new URL(raw);
|
|
149
|
+
} catch (_) {
|
|
150
|
+
return { ok: false, error: "invalid-target" };
|
|
151
|
+
}
|
|
152
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
153
|
+
return { ok: false, error: "invalid-target" };
|
|
154
|
+
}
|
|
155
|
+
if (!isPrivateOrLoopbackHostname(parsed.hostname)) {
|
|
156
|
+
return { ok: false, error: "non-private-target" };
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, url: raw };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function decodeFragment(fragment) {
|
|
162
|
+
var raw = String(fragment || "").replace(/^#/, "").trim();
|
|
163
|
+
if (!raw) return { ok: false, error: "missing-fragment" };
|
|
164
|
+
var m = /^v1\.([A-Za-z0-9_-]+)$/.exec(raw);
|
|
165
|
+
if (!m) return { ok: false, error: "unsupported-version" };
|
|
166
|
+
try {
|
|
167
|
+
var b64 = m[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
168
|
+
while (b64.length % 4) b64 += "=";
|
|
169
|
+
var json = atob(b64);
|
|
170
|
+
var parsed = JSON.parse(json);
|
|
171
|
+
if (!parsed || parsed.v !== 1 || typeof parsed.u !== "string") {
|
|
172
|
+
return { ok: false, error: "invalid-payload" };
|
|
173
|
+
}
|
|
174
|
+
var target = validateTarget(parsed.u.trim());
|
|
175
|
+
if (!target.ok) return target;
|
|
176
|
+
if (parsed.e != null) {
|
|
177
|
+
var e = Number(parsed.e);
|
|
178
|
+
if (!isFinite(e)) return { ok: false, error: "invalid-expiry" };
|
|
179
|
+
if (Math.floor(Date.now() / 1000) > e) return { ok: false, error: "expired" };
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, url: target.url };
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { ok: false, error: "invalid-payload" };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var decoded = decodeFragment(location.hash);
|
|
188
|
+
if (!decoded.ok) {
|
|
189
|
+
var map = {
|
|
190
|
+
"missing-fragment": "This share link is incomplete. Ask the operator to re-run dashboard-broadcast.",
|
|
191
|
+
"unsupported-version": "This share link uses an unsupported format.",
|
|
192
|
+
"invalid-payload": "This share link could not be decoded.",
|
|
193
|
+
"invalid-target": "This share link has an invalid Mission Control URL.",
|
|
194
|
+
"non-private-target": "This share link targets a non-private host and was blocked.",
|
|
195
|
+
expired: "This share link expired. Ask the operator for a fresh link.",
|
|
196
|
+
};
|
|
197
|
+
fail(map[decoded.error] || "Could not open Mission Control.");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
openEl.href = decoded.url;
|
|
202
|
+
openEl.hidden = false;
|
|
203
|
+
statusEl.textContent = "Ready. Continue to Mission Control on your LAN.";
|
|
204
|
+
// Auto-navigate only for validated private/loopback targets; button remains if blocked.
|
|
205
|
+
try {
|
|
206
|
+
location.replace(decoded.url);
|
|
207
|
+
} catch (_) {
|
|
208
|
+
/* keep button */
|
|
209
|
+
}
|
|
210
|
+
})();
|
|
211
|
+
</script>
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
package/dashboard/serve.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, watch, writeFileSync
|
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
import { dirname, extname, join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { shareShellTokenRequired } from "./lib/broadcast-share.mjs";
|
|
13
14
|
import {
|
|
14
15
|
DEFAULT_HOST,
|
|
15
16
|
REPO_ROOT_ENV,
|
|
@@ -423,8 +424,10 @@ const server = createServer((req, res) => {
|
|
|
423
424
|
return;
|
|
424
425
|
}
|
|
425
426
|
|
|
427
|
+
// Cosmetic share resolver shell (fragment holds LAN+token client-side).
|
|
428
|
+
// ADR: 2026-08-11_mission-control-broadcast-url-mask.md
|
|
426
429
|
const auth = authorizeMissionControlRequest(req, url, {
|
|
427
|
-
tokenRequired: TOKEN_REQUIRED,
|
|
430
|
+
tokenRequired: shareShellTokenRequired(TOKEN_REQUIRED, req.method || "GET", path),
|
|
428
431
|
expectedToken: AUTH_TOKEN,
|
|
429
432
|
});
|
|
430
433
|
if (!auth.ok) {
|
|
@@ -496,7 +499,8 @@ const server = createServer((req, res) => {
|
|
|
496
499
|
return;
|
|
497
500
|
}
|
|
498
501
|
|
|
499
|
-
const
|
|
502
|
+
const staticLookup = path === "/open" ? "/open.html" : path;
|
|
503
|
+
const staticPath = resolveStaticPath(staticLookup);
|
|
500
504
|
if (!staticPath) {
|
|
501
505
|
res.writeHead(404);
|
|
502
506
|
res.end("Not found");
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
11
11
|
import { existsSync, openSync } from "node:fs";
|
|
12
|
-
import {
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
14
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import {
|
|
15
|
+
buildBroadcastShareUrl,
|
|
16
|
+
resolveShareBase,
|
|
17
|
+
resolveShareShowLan,
|
|
18
|
+
resolveShareTtlSec,
|
|
19
|
+
} from "./lib/broadcast-share.mjs";
|
|
15
20
|
import {
|
|
16
21
|
BROADCAST_TOKEN_ENV,
|
|
17
22
|
escapePerlDoubleQuoted,
|
|
@@ -21,10 +26,16 @@ import {
|
|
|
21
26
|
listLanIPv4Addresses,
|
|
22
27
|
normalizeAuthToken,
|
|
23
28
|
resolveBindHost,
|
|
29
|
+
resolveContextConfigPath,
|
|
30
|
+
resolveSnapshotRepoRoot,
|
|
24
31
|
} from "./lib/guards.mjs";
|
|
32
|
+
import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.mjs";
|
|
25
33
|
|
|
26
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
const
|
|
35
|
+
const KIT_ROOT = join(__dirname, "..");
|
|
36
|
+
/** Workspace snapshots / preference config. Defaults to KIT_ROOT. */
|
|
37
|
+
const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
|
|
38
|
+
process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
|
|
28
39
|
const SERVE = join(__dirname, "serve.mjs");
|
|
29
40
|
const LOG = process.env.MISSION_CONTROL_LOG || "/tmp/mission-control-broadcast.log";
|
|
30
41
|
const PORT = Number.parseInt(process.env.PORT || "3333", 10);
|
|
@@ -97,7 +108,7 @@ function detachStart(env) {
|
|
|
97
108
|
if (hasSetsid()) {
|
|
98
109
|
const out = openSync(LOG, "a");
|
|
99
110
|
const child = spawn("setsid", ["node", SERVE], {
|
|
100
|
-
cwd:
|
|
111
|
+
cwd: KIT_ROOT,
|
|
101
112
|
detached: true,
|
|
102
113
|
stdio: ["ignore", out, out],
|
|
103
114
|
env,
|
|
@@ -107,7 +118,7 @@ function detachStart(env) {
|
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
// Escape @/$ so scoped package paths (node_modules/@scope/...) survive Perl qq.
|
|
110
|
-
const rootEsc = escapePerlDoubleQuoted(
|
|
121
|
+
const rootEsc = escapePerlDoubleQuoted(KIT_ROOT);
|
|
111
122
|
const serveEsc = escapePerlDoubleQuoted(SERVE);
|
|
112
123
|
const logEsc = escapePerlDoubleQuoted(LOG);
|
|
113
124
|
const hostEsc = escapePerlDoubleQuoted(String(env.HOST));
|
|
@@ -129,7 +140,7 @@ function detachStart(env) {
|
|
|
129
140
|
].join(" ");
|
|
130
141
|
|
|
131
142
|
const child = spawn("perl", ["-e", perl], {
|
|
132
|
-
cwd:
|
|
143
|
+
cwd: KIT_ROOT,
|
|
133
144
|
detached: true,
|
|
134
145
|
stdio: "ignore",
|
|
135
146
|
env,
|
|
@@ -148,24 +159,6 @@ async function waitReady(urls) {
|
|
|
148
159
|
return null;
|
|
149
160
|
}
|
|
150
161
|
|
|
151
|
-
function openBrowser(url) {
|
|
152
|
-
const os = platform();
|
|
153
|
-
try {
|
|
154
|
-
if (os === "darwin") {
|
|
155
|
-
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
if (os === "win32") {
|
|
159
|
-
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
163
|
-
return true;
|
|
164
|
-
} catch {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
162
|
async function main() {
|
|
170
163
|
const { env, host, token } = resolveBroadcastEnv();
|
|
171
164
|
if (isLoopbackBindHost(host)) {
|
|
@@ -203,14 +196,43 @@ async function main() {
|
|
|
203
196
|
console.log(`Mission Control broadcast already listening on port ${PORT}`);
|
|
204
197
|
}
|
|
205
198
|
|
|
199
|
+
const shareBase = resolveShareBase(process.env);
|
|
200
|
+
let shareUrl = null;
|
|
201
|
+
if (shareBase != null) {
|
|
202
|
+
try {
|
|
203
|
+
shareUrl = buildBroadcastShareUrl(displayUrl, {
|
|
204
|
+
base: shareBase,
|
|
205
|
+
ttlSec: resolveShareTtlSec(process.env),
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
// Non-RFC1918 primary LAN (Tailscale 100.64/10, public/DMZ) cannot encode into
|
|
209
|
+
// the share fragment allowlist — degrade to LAN/token print instead of exit 1.
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
+
console.warn(`Share URL skipped (${msg}). Printing LAN/token only.`);
|
|
212
|
+
shareUrl = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const showLan = resolveShareShowLan(process.env);
|
|
216
|
+
const openTarget = shareUrl || displayUrl;
|
|
217
|
+
|
|
206
218
|
console.log("");
|
|
207
219
|
console.log(" Mission Control (LAN broadcast)");
|
|
208
220
|
console.log(` Bind: ${host}:${PORT}`);
|
|
221
|
+
if (shareUrl) {
|
|
222
|
+
console.log(` Share: ${shareUrl}`);
|
|
223
|
+
}
|
|
209
224
|
console.log(` Token: ${token}`);
|
|
210
|
-
|
|
211
|
-
|
|
225
|
+
if (showLan || !shareUrl) {
|
|
226
|
+
for (const ip of listLanIPv4Addresses()) {
|
|
227
|
+
console.log(` LAN: http://${ip}:${PORT}/?token=${encodeURIComponent(token)}`);
|
|
228
|
+
}
|
|
229
|
+
console.log(` Local: http://127.0.0.1:${PORT}/?token=${encodeURIComponent(token)}`);
|
|
230
|
+
}
|
|
231
|
+
if (shareUrl) {
|
|
232
|
+
console.log(
|
|
233
|
+
" Share is a cosmetic Mission Kit (or BYO) link; phone must still reach this LAN.",
|
|
234
|
+
);
|
|
212
235
|
}
|
|
213
|
-
console.log(` Local: http://127.0.0.1:${PORT}/?token=${encodeURIComponent(token)}`);
|
|
214
236
|
console.log(" Config writes stay loopback-only. Stop: kill the LISTEN pid on this port.");
|
|
215
237
|
console.log(" Firewall: allow inbound TCP on this port for your LAN profile if needed.");
|
|
216
238
|
console.log("");
|
|
@@ -218,10 +240,32 @@ async function main() {
|
|
|
218
240
|
if (process.env.MISSION_CONTROL_NO_OPEN === "1") {
|
|
219
241
|
return;
|
|
220
242
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
243
|
+
let configValue = null;
|
|
244
|
+
const cfg = resolveContextConfigPath(ROOT, { existsSync });
|
|
245
|
+
if (cfg.ok) {
|
|
246
|
+
configValue = readPreferredBrowserFromConfig(cfg.path);
|
|
247
|
+
}
|
|
248
|
+
const result = openBrowser(openTarget, { configValue });
|
|
249
|
+
if (result.opened) {
|
|
250
|
+
if (result.reason === "preferred-fallback") {
|
|
251
|
+
console.log(
|
|
252
|
+
shareUrl
|
|
253
|
+
? "Preferred browser failed; opened share URL with the OS default."
|
|
254
|
+
: "Preferred browser failed; opened primary URL with the OS default.",
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
console.log(
|
|
258
|
+
shareUrl
|
|
259
|
+
? "Opened share URL in the preferred browser (or OS default)."
|
|
260
|
+
: "Opened primary URL in the preferred browser (or OS default).",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
} else if (result.reason !== "no-open") {
|
|
264
|
+
console.log(
|
|
265
|
+
shareUrl
|
|
266
|
+
? "Open the Share URL above on your phone/tablet browser."
|
|
267
|
+
: "Open a LAN URL above on your phone/tablet browser.",
|
|
268
|
+
);
|
|
225
269
|
}
|
|
226
270
|
}
|
|
227
271
|
|
package/dashboard/start.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Allocates a stable per-workspace listen port (hash of snapshot root in the
|
|
6
6
|
* 3333–3588 range unless PORT is set), detach-starts `serve.mjs` when needed,
|
|
7
|
-
* waits until HTTP 200, prints the URL, and opens
|
|
7
|
+
* waits until HTTP 200, prints the URL, and opens one preferred browser
|
|
8
|
+
* (or OS default). Never opens more than one browser process.
|
|
8
9
|
*
|
|
9
10
|
* Never kills a listener whose system.repoRoot belongs to another workspace.
|
|
10
11
|
*
|
|
@@ -17,21 +18,23 @@
|
|
|
17
18
|
|
|
18
19
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
19
20
|
import { existsSync, openSync } from "node:fs";
|
|
20
|
-
import {
|
|
21
|
-
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import {
|
|
24
24
|
REPO_ROOT_ENV,
|
|
25
25
|
escapePerlDoubleQuoted,
|
|
26
26
|
repoRootLogId,
|
|
27
|
+
resolveContextConfigPath,
|
|
27
28
|
resolveMissionControlPort,
|
|
28
29
|
resolveSnapshotRepoRoot,
|
|
29
30
|
sameRepoRoot,
|
|
30
31
|
} from "./lib/guards.mjs";
|
|
32
|
+
import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.mjs";
|
|
31
33
|
|
|
32
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
35
|
const KIT_ROOT = join(__dirname, "..");
|
|
34
36
|
const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
|
|
37
|
+
process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
|
|
35
38
|
const SERVE = join(__dirname, "serve.mjs");
|
|
36
39
|
const HOST = process.env.HOST || "127.0.0.1";
|
|
37
40
|
const DISPLAY_HOST = HOST === "0.0.0.0" ? "127.0.0.1" : HOST;
|
|
@@ -188,24 +191,6 @@ async function waitReady() {
|
|
|
188
191
|
return false;
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
function openBrowser(url) {
|
|
192
|
-
const os = platform();
|
|
193
|
-
try {
|
|
194
|
-
if (os === "darwin") {
|
|
195
|
-
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
if (os === "win32") {
|
|
199
|
-
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
203
|
-
return true;
|
|
204
|
-
} catch {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
194
|
async function ensureServer() {
|
|
210
195
|
const allocation = resolveMissionControlPort({
|
|
211
196
|
repoRoot: ROOT,
|
|
@@ -271,11 +256,23 @@ async function main() {
|
|
|
271
256
|
if (process.env.MISSION_CONTROL_NO_OPEN === "1") {
|
|
272
257
|
return;
|
|
273
258
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
278
|
-
}
|
|
259
|
+
let configValue = null;
|
|
260
|
+
const cfg = resolveContextConfigPath(ROOT, { existsSync });
|
|
261
|
+
if (cfg.ok) {
|
|
262
|
+
configValue = readPreferredBrowserFromConfig(cfg.path);
|
|
263
|
+
}
|
|
264
|
+
const result = openBrowser(URL, { configValue });
|
|
265
|
+
if (result.opened) {
|
|
266
|
+
if (result.reason === "preferred-fallback") {
|
|
267
|
+
console.log(
|
|
268
|
+
"Preferred browser failed; opened with the OS default. In Cursor, Simple Browser or /dashboard also works.",
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
console.log(
|
|
272
|
+
"Opened in the preferred browser (or OS default). In Cursor, Simple Browser or /dashboard also works.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
} else if (result.reason !== "no-open") {
|
|
279
276
|
console.log("Open that URL in a browser (Cursor: Simple Browser, or run /dashboard in chat).");
|
|
280
277
|
}
|
|
281
278
|
}
|