minecodex 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/LICENSE +21 -0
- package/README.md +56 -0
- package/apps/dashboard/app.js +387 -0
- package/apps/dashboard/favicon.svg +18 -0
- package/apps/dashboard/index.html +70 -0
- package/apps/dashboard/styles.css +297 -0
- package/features/images/README.md +106 -0
- package/features/images/codex-feature.json +24 -0
- package/features/images/src/http-server.mjs +302 -0
- package/features/images/src/image-library.mjs +582 -0
- package/features/images/src/main.mjs +52 -0
- package/features/images/src/prompt-index.mjs +182 -0
- package/features/images/src/save-as.mjs +40 -0
- package/features/images/src/thread-metadata.mjs +93 -0
- package/features/images/web/app.js +1045 -0
- package/features/images/web/index.html +162 -0
- package/features/images/web/styles.css +994 -0
- package/features/model-slider/README.md +30 -0
- package/features/model-slider/THIRD_PARTY_NOTICES.md +24 -0
- package/features/model-slider/assets/providers/anthropic.svg +1 -0
- package/features/model-slider/assets/providers/deepseek.svg +1 -0
- package/features/model-slider/assets/providers/gemini.svg +1 -0
- package/features/model-slider/assets/providers/grok.svg +1 -0
- package/features/model-slider/assets/providers/hunyuan.svg +1 -0
- package/features/model-slider/assets/providers/kimi.svg +1 -0
- package/features/model-slider/assets/providers/minimax.svg +1 -0
- package/features/model-slider/assets/providers/openai.svg +1 -0
- package/features/model-slider/assets/providers/qwen.svg +1 -0
- package/features/model-slider/assets/providers/xiaomimimo.svg +1 -0
- package/features/model-slider/assets/providers/zhipu.svg +1 -0
- package/features/model-slider/assets/ui/sparkles.svg +18 -0
- package/features/model-slider/assets/ui/star-off.svg +17 -0
- package/features/model-slider/assets/ui/star.svg +15 -0
- package/features/model-slider/codex-feature.json +56 -0
- package/features/model-slider/vendor/cc-switch-LICENSE +21 -0
- package/features/model-slider/vendor/lucide-LICENSE +35 -0
- package/features/notes/README.md +106 -0
- package/features/notes/assets/icons/circle.svg +15 -0
- package/features/notes/assets/icons/ellipsis.svg +17 -0
- package/features/notes/assets/icons/external-link.svg +17 -0
- package/features/notes/assets/icons/list-todo.svg +19 -0
- package/features/notes/assets/icons/message-square-quote.svg +17 -0
- package/features/notes/assets/icons/paperclip.svg +15 -0
- package/features/notes/assets/icons/star-off.svg +17 -0
- package/features/notes/assets/icons/star.svg +15 -0
- package/features/notes/assets/icons/sticky-note.svg +16 -0
- package/features/notes/assets/icons/trash-2.svg +19 -0
- package/features/notes/codex-feature.json +90 -0
- package/features/notes/src/http-server.mjs +541 -0
- package/features/notes/src/main.mjs +54 -0
- package/features/notes/src/product.mjs +771 -0
- package/features/notes/web/app.js +1495 -0
- package/features/notes/web/codex-artifact-document.svg +1 -0
- package/features/notes/web/codex-attachment.svg +1 -0
- package/features/notes/web/codex-build.svg +1 -0
- package/features/notes/web/codex-code.svg +1 -0
- package/features/notes/web/codex-cplusplus.svg +1 -0
- package/features/notes/web/codex-css.svg +1 -0
- package/features/notes/web/codex-document.svg +1 -0
- package/features/notes/web/codex-file.svg +1 -0
- package/features/notes/web/codex-folder.svg +1 -0
- package/features/notes/web/codex-hashes.svg +1 -0
- package/features/notes/web/codex-html.svg +1 -0
- package/features/notes/web/codex-image.svg +1 -0
- package/features/notes/web/codex-java.svg +1 -0
- package/features/notes/web/codex-javascript.svg +1 -0
- package/features/notes/web/codex-json.svg +1 -0
- package/features/notes/web/codex-notebook.svg +1 -0
- package/features/notes/web/codex-pdf.svg +1 -0
- package/features/notes/web/codex-pencil.svg +3 -0
- package/features/notes/web/codex-php.svg +1 -0
- package/features/notes/web/codex-plus.svg +3 -0
- package/features/notes/web/codex-presentation.svg +1 -0
- package/features/notes/web/codex-python.svg +1 -0
- package/features/notes/web/codex-react.svg +1 -0
- package/features/notes/web/codex-rust.svg +1 -0
- package/features/notes/web/codex-shell.svg +1 -0
- package/features/notes/web/codex-skill.svg +1 -0
- package/features/notes/web/codex-spreadsheet.svg +1 -0
- package/features/notes/web/codex-task.svg +3 -0
- package/features/notes/web/codex-terminal.svg +1 -0
- package/features/notes/web/codex-toml.svg +1 -0
- package/features/notes/web/codex-typescript.svg +1 -0
- package/features/notes/web/codex-view-all.svg +3 -0
- package/features/notes/web/index.html +15 -0
- package/features/notes/web/styles.css +1155 -0
- package/package.json +67 -0
- package/packages/cli/bin/mcx.mjs +7 -0
- package/packages/cli/src/browser-exchange.mjs +31 -0
- package/packages/cli/src/commands.mjs +389 -0
- package/packages/cli/src/config.mjs +101 -0
- package/packages/cli/src/control-server.mjs +371 -0
- package/packages/cli/src/npm-adapter.mjs +40 -0
- package/packages/cli/src/paths.mjs +33 -0
- package/packages/cli/src/platform.mjs +368 -0
- package/packages/cli/src/runtime-manager.mjs +338 -0
- package/packages/runtime-host/README.md +171 -0
- package/packages/runtime-host/src/codex-runtime.mjs +4311 -0
- package/packages/runtime-host/src/feature-registry.mjs +827 -0
- package/packages/runtime-host/src/main.mjs +98 -0
- package/packages/runtime-host/src/pid-file.mjs +23 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { BrowserExchangeStore } from "./browser-exchange.mjs";
|
|
9
|
+
import { FEATURE_IDS, readConfig, updateFeature } from "./config.mjs";
|
|
10
|
+
import { normalizeRuntimeReady } from "./runtime-manager.mjs";
|
|
11
|
+
|
|
12
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
13
|
+
const packageMetadata = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
14
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
15
|
+
const CONTENT_TYPES = new Map([
|
|
16
|
+
[".css", "text/css; charset=utf-8"],
|
|
17
|
+
[".html", "text/html; charset=utf-8"],
|
|
18
|
+
[".js", "text/javascript; charset=utf-8"],
|
|
19
|
+
[".svg", "image/svg+xml"],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function tokenMatches(candidate, expected) {
|
|
23
|
+
if (!candidate || !expected) return false;
|
|
24
|
+
const left = createHash("sha256").update(candidate).digest();
|
|
25
|
+
const right = createHash("sha256").update(expected).digest();
|
|
26
|
+
return timingSafeEqual(left, right);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseCookies(header = "") {
|
|
30
|
+
return Object.fromEntries(header.split(";").flatMap((part) => {
|
|
31
|
+
const index = part.indexOf("=");
|
|
32
|
+
if (index < 1) return [];
|
|
33
|
+
return [[part.slice(0, index).trim(), decodeURIComponent(part.slice(index + 1).trim())]];
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requestToken(request) {
|
|
38
|
+
const authorization = request.headers.authorization ?? "";
|
|
39
|
+
if (authorization.startsWith("Bearer ")) return authorization.slice(7);
|
|
40
|
+
return parseCookies(request.headers.cookie).mcx_session ?? "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function json(response, statusCode, value) {
|
|
44
|
+
const body = `${JSON.stringify(value)}\n`;
|
|
45
|
+
response.writeHead(statusCode, {
|
|
46
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
47
|
+
"Content-Length": Buffer.byteLength(body),
|
|
48
|
+
"Cache-Control": "no-store",
|
|
49
|
+
});
|
|
50
|
+
response.end(body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readJson(request) {
|
|
54
|
+
let size = 0;
|
|
55
|
+
const chunks = [];
|
|
56
|
+
for await (const chunk of request) {
|
|
57
|
+
size += chunk.length;
|
|
58
|
+
if (size > MAX_BODY_BYTES) throw Object.assign(new Error("Request body is too large"), { statusCode: 413 });
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
}
|
|
61
|
+
let value;
|
|
62
|
+
try {
|
|
63
|
+
value = JSON.parse(Buffer.concat(chunks).toString("utf8") || "null");
|
|
64
|
+
} catch {
|
|
65
|
+
throw Object.assign(new Error("Request body must be valid JSON"), { statusCode: 400 });
|
|
66
|
+
}
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
68
|
+
throw Object.assign(new Error("Request body must be a JSON object"), { statusCode: 400 });
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function localized(value, locale) {
|
|
74
|
+
if (typeof value === "string") return value;
|
|
75
|
+
return value?.[locale] ?? value?.en ?? Object.values(value ?? {})[0] ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function acceptedHealth(featureId, health) {
|
|
79
|
+
if (health === true || typeof health !== "object" || health === null || Array.isArray(health)) return false;
|
|
80
|
+
const identity = health.identity ?? health;
|
|
81
|
+
return Boolean(
|
|
82
|
+
identity
|
|
83
|
+
&& identity.service === featureId
|
|
84
|
+
&& identity.protocolVersion === 1
|
|
85
|
+
&& typeof identity.instanceId === "string"
|
|
86
|
+
&& identity.instanceId.length > 0
|
|
87
|
+
&& health.ok === true
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function loadFeatureDefinitions(paths, locale = "en") {
|
|
92
|
+
return Promise.all(FEATURE_IDS.map(async (id) => {
|
|
93
|
+
const manifestPath = path.join(paths.featuresRoot, id, "codex-feature.json");
|
|
94
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
version: manifest.version ?? packageMetadata.version,
|
|
98
|
+
label: localized(manifest.label, locale),
|
|
99
|
+
description: localized(manifest.description, locale),
|
|
100
|
+
iconUrl: `/api/icons/${id}.svg`,
|
|
101
|
+
healthUrl: manifest.healthUrl ?? manifest.surfaceUrl ?? null,
|
|
102
|
+
};
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function featureStatuses(paths, runtimeManager, locale, runtimeState = null) {
|
|
107
|
+
const config = await readConfig(paths.configPath);
|
|
108
|
+
const runtime = runtimeState ?? await (runtimeManager.refreshStatus?.() ?? runtimeManager.status());
|
|
109
|
+
const definitions = await loadFeatureDefinitions(paths, locale);
|
|
110
|
+
const plugins = runtime.plugins ?? runtime.pluginStates ?? [];
|
|
111
|
+
const appliedFeatures = Array.isArray(runtime.appliedFeatures) ? runtime.appliedFeatures : [];
|
|
112
|
+
const ready = normalizeRuntimeReady(runtime.ready ?? {
|
|
113
|
+
runtimeSessionId: runtime.runtimeSessionId,
|
|
114
|
+
features: appliedFeatures,
|
|
115
|
+
plugins,
|
|
116
|
+
renderer: runtime.renderer,
|
|
117
|
+
failures: runtime.failures,
|
|
118
|
+
});
|
|
119
|
+
const renderer = ready.renderer;
|
|
120
|
+
const rendererCurrent = Boolean(
|
|
121
|
+
renderer?.discovered
|
|
122
|
+
&& renderer.runtimeSessionId
|
|
123
|
+
&& renderer.runtimeSessionId === ready.runtimeSessionId
|
|
124
|
+
&& renderer.active
|
|
125
|
+
&& Number(renderer.injectedTargets) > 0,
|
|
126
|
+
);
|
|
127
|
+
return definitions.map((feature) => {
|
|
128
|
+
const plugin = ready.plugins.find((candidate) => candidate.featureId === feature.id) ?? null;
|
|
129
|
+
const pluginState = plugin?.state ?? null;
|
|
130
|
+
const applied = appliedFeatures.includes(feature.id)
|
|
131
|
+
|| ["configured", "launching", "ready", "injected"].includes(pluginState);
|
|
132
|
+
const injected = Boolean(
|
|
133
|
+
pluginState === "injected" && rendererCurrent,
|
|
134
|
+
);
|
|
135
|
+
const healthy = Boolean(
|
|
136
|
+
injected
|
|
137
|
+
&& acceptedHealth(feature.id, plugin?.health),
|
|
138
|
+
);
|
|
139
|
+
let state;
|
|
140
|
+
const pluginFailed = Boolean(
|
|
141
|
+
pluginState === "failed"
|
|
142
|
+
|| plugin?.failure,
|
|
143
|
+
);
|
|
144
|
+
if (!runtime.running) {
|
|
145
|
+
state = "stopped";
|
|
146
|
+
} else if (!config.features[feature.id] && applied) {
|
|
147
|
+
state = "restart-required";
|
|
148
|
+
} else if (!config.features[feature.id] || pluginState === "stopped") {
|
|
149
|
+
state = "stopped";
|
|
150
|
+
} else if (pluginFailed) {
|
|
151
|
+
state = "failed";
|
|
152
|
+
} else if (config.features[feature.id] !== applied) {
|
|
153
|
+
state = "restart-required";
|
|
154
|
+
} else if (pluginState === "configured" || pluginState === "launching" || pluginState === "ready") {
|
|
155
|
+
state = "launching";
|
|
156
|
+
} else if (injected && healthy) {
|
|
157
|
+
state = "healthy";
|
|
158
|
+
} else if (injected) {
|
|
159
|
+
state = "injected";
|
|
160
|
+
} else if (pluginState === "injected") {
|
|
161
|
+
state = feature.healthUrl ? "launching" : "failed";
|
|
162
|
+
} else {
|
|
163
|
+
state = "launching";
|
|
164
|
+
}
|
|
165
|
+
const failure = plugin?.failure
|
|
166
|
+
?? (state === "failed" && pluginState === "injected" && !rendererCurrent
|
|
167
|
+
? {
|
|
168
|
+
code: "CODEX_RENDERER_UNAVAILABLE",
|
|
169
|
+
message: "The matching Codex renderer is unavailable; restart Codex.",
|
|
170
|
+
phase: "renderer",
|
|
171
|
+
}
|
|
172
|
+
: null);
|
|
173
|
+
return {
|
|
174
|
+
...feature,
|
|
175
|
+
enabled: config.features[feature.id],
|
|
176
|
+
applied,
|
|
177
|
+
injected,
|
|
178
|
+
healthy,
|
|
179
|
+
state,
|
|
180
|
+
health: plugin?.health ?? null,
|
|
181
|
+
failure,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requireMutationOrigin(request, host, port) {
|
|
187
|
+
const expectedHost = `${host}:${port}`;
|
|
188
|
+
const origin = request.headers.origin;
|
|
189
|
+
if (request.headers.host !== expectedHost || origin !== `http://${expectedHost}`) {
|
|
190
|
+
throw Object.assign(new Error("Mutation Origin is not allowed"), { statusCode: 403 });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function iconResponse(id, paths, response) {
|
|
195
|
+
let iconPath;
|
|
196
|
+
if (id === "model-slider") {
|
|
197
|
+
iconPath = path.join(paths.featuresRoot, id, "assets", "ui", "sparkles.svg");
|
|
198
|
+
} else if (id === "notes") {
|
|
199
|
+
iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "sticky-note.svg");
|
|
200
|
+
} else if (id === "images") {
|
|
201
|
+
const manifest = JSON.parse(await readFile(path.join(paths.featuresRoot, id, "codex-feature.json"), "utf8"));
|
|
202
|
+
const viewBox = manifest.icon?.viewBox ?? "0 0 24 24";
|
|
203
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${manifest.icon.markup}</svg>`;
|
|
204
|
+
response.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" });
|
|
205
|
+
response.end(svg);
|
|
206
|
+
return;
|
|
207
|
+
} else {
|
|
208
|
+
json(response, 404, { error: "NOT_FOUND" });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
response.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" });
|
|
212
|
+
await pipeline(createReadStream(iconPath), response);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function staticResponse(url, paths, response) {
|
|
216
|
+
const requested = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
|
|
217
|
+
const absolute = path.resolve(paths.dashboardDir, requested);
|
|
218
|
+
if (absolute !== paths.dashboardDir && !absolute.startsWith(`${paths.dashboardDir}${path.sep}`)) {
|
|
219
|
+
json(response, 404, { error: "NOT_FOUND" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const info = await stat(absolute);
|
|
224
|
+
if (!info.isFile()) throw Object.assign(new Error("Not a file"), { code: "ENOENT" });
|
|
225
|
+
response.writeHead(200, {
|
|
226
|
+
"Content-Type": CONTENT_TYPES.get(path.extname(absolute)) ?? "application/octet-stream",
|
|
227
|
+
"Content-Length": info.size,
|
|
228
|
+
"Cache-Control": "no-store",
|
|
229
|
+
"Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; frame-ancestors 'none'",
|
|
230
|
+
"X-Content-Type-Options": "nosniff",
|
|
231
|
+
});
|
|
232
|
+
await pipeline(createReadStream(absolute), response);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error.code === "ENOENT") json(response, 404, { error: "NOT_FOUND" });
|
|
235
|
+
else throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function createControlServer({
|
|
240
|
+
paths,
|
|
241
|
+
token,
|
|
242
|
+
runtimeManager,
|
|
243
|
+
host = "127.0.0.1",
|
|
244
|
+
port = 47830,
|
|
245
|
+
logger = console,
|
|
246
|
+
exchangeStore = new BrowserExchangeStore(),
|
|
247
|
+
}) {
|
|
248
|
+
let boundPort = port;
|
|
249
|
+
const server = createServer(async (request, response) => {
|
|
250
|
+
try {
|
|
251
|
+
const url = new URL(request.url, `http://${host}:${boundPort}`);
|
|
252
|
+
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
253
|
+
json(response, 200, { service: "minecodex", version: packageMetadata.version, ok: true });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (request.method === "GET" && url.pathname === "/auth") {
|
|
257
|
+
let sessionToken;
|
|
258
|
+
try {
|
|
259
|
+
sessionToken = exchangeStore.consume(url.searchParams.get("exchange"));
|
|
260
|
+
} catch {
|
|
261
|
+
json(response, 403, { error: "INVALID_EXCHANGE" });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
response.writeHead(303, {
|
|
265
|
+
Location: "/",
|
|
266
|
+
"Set-Cookie": `mcx_session=${encodeURIComponent(sessionToken)}; HttpOnly; SameSite=Strict; Path=/`,
|
|
267
|
+
"Referrer-Policy": "no-referrer",
|
|
268
|
+
"Cache-Control": "no-store",
|
|
269
|
+
});
|
|
270
|
+
response.end();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (url.pathname.startsWith("/api/") && !tokenMatches(requestToken(request), token)) {
|
|
274
|
+
json(response, 401, { error: "UNAUTHORIZED" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const locale = String(request.headers["accept-language"] ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
|
|
278
|
+
if (request.method === "GET" && url.pathname === "/api/status") {
|
|
279
|
+
const runtime = await (runtimeManager.refreshStatus?.() ?? runtimeManager.status());
|
|
280
|
+
const features = await featureStatuses(paths, runtimeManager, locale, runtime);
|
|
281
|
+
json(response, 200, {
|
|
282
|
+
version: packageMetadata.version,
|
|
283
|
+
service: { running: true },
|
|
284
|
+
runtime: {
|
|
285
|
+
running: runtime.running,
|
|
286
|
+
pid: runtime.pid ?? null,
|
|
287
|
+
runtimeSessionId: runtime.runtimeSessionId ?? null,
|
|
288
|
+
ready: runtime.ready ?? null,
|
|
289
|
+
plugins: runtime.plugins ?? runtime.pluginStates ?? [],
|
|
290
|
+
renderer: runtime.renderer ?? null,
|
|
291
|
+
failures: runtime.failures ?? [],
|
|
292
|
+
},
|
|
293
|
+
runtimeSessionId: runtime.runtimeSessionId ?? null,
|
|
294
|
+
renderer: runtime.renderer ?? null,
|
|
295
|
+
pendingRestart: features.some((feature) => feature.state === "restart-required"),
|
|
296
|
+
features,
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (request.method === "GET" && url.pathname === "/api/features") {
|
|
301
|
+
json(response, 200, { features: await featureStatuses(paths, runtimeManager, locale) });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (request.method === "GET" && url.pathname === "/api/update") {
|
|
305
|
+
const registryResponse = await fetch("https://registry.npmjs.org/minecodex/latest", {
|
|
306
|
+
signal: AbortSignal.timeout(4_000),
|
|
307
|
+
});
|
|
308
|
+
if (!registryResponse.ok) throw Object.assign(new Error("Update check failed"), { statusCode: 502 });
|
|
309
|
+
const latest = String((await registryResponse.json()).version ?? "");
|
|
310
|
+
json(response, 200, {
|
|
311
|
+
current: packageMetadata.version,
|
|
312
|
+
latest,
|
|
313
|
+
available: Boolean(latest && latest !== packageMetadata.version),
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (request.method === "POST" && url.pathname === "/api/auth/exchange") {
|
|
318
|
+
requireMutationOrigin(request, host, boundPort);
|
|
319
|
+
await readJson(request);
|
|
320
|
+
json(response, 200, { exchange: exchangeStore.issue(token), expiresInMs: exchangeStore.ttlMs });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const featureMatch = url.pathname.match(/^\/api\/features\/([a-z-]+)$/);
|
|
324
|
+
if (request.method === "PATCH" && featureMatch) {
|
|
325
|
+
requireMutationOrigin(request, host, boundPort);
|
|
326
|
+
const body = await readJson(request);
|
|
327
|
+
await updateFeature(paths.configPath, featureMatch[1], body.enabled);
|
|
328
|
+
const features = await featureStatuses(paths, runtimeManager, locale);
|
|
329
|
+
json(response, 200, {
|
|
330
|
+
saved: true,
|
|
331
|
+
pendingRestart: features.some((feature) => feature.state === "restart-required"),
|
|
332
|
+
features,
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (request.method === "POST" && url.pathname === "/api/restart") {
|
|
337
|
+
requireMutationOrigin(request, host, boundPort);
|
|
338
|
+
await readJson(request);
|
|
339
|
+
await runtimeManager.restart();
|
|
340
|
+
json(response, 200, { restarted: true, features: await featureStatuses(paths, runtimeManager, locale) });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const iconMatch = url.pathname.match(/^\/api\/icons\/([a-z-]+)\.svg$/);
|
|
344
|
+
if (request.method === "GET" && iconMatch) {
|
|
345
|
+
await iconResponse(iconMatch[1], paths, response);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
349
|
+
await staticResponse(url, paths, response);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
json(response, 404, { error: "NOT_FOUND" });
|
|
353
|
+
} catch (error) {
|
|
354
|
+
logger.error(error);
|
|
355
|
+
if (!response.headersSent) json(response, error.statusCode ?? 500, { error: error.message });
|
|
356
|
+
else response.destroy(error);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
await new Promise((resolve, reject) => {
|
|
360
|
+
server.once("error", reject);
|
|
361
|
+
server.listen(port, host, resolve);
|
|
362
|
+
});
|
|
363
|
+
const address = server.address();
|
|
364
|
+
boundPort = address.port;
|
|
365
|
+
return {
|
|
366
|
+
host,
|
|
367
|
+
port: address.port,
|
|
368
|
+
url: `http://${host}:${address.port}`,
|
|
369
|
+
close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const defaultExecFile = promisify(execFileCallback);
|
|
8
|
+
|
|
9
|
+
export function createNpmAdapter({ execFile = defaultExecFile, tempRoot = os.tmpdir() } = {}) {
|
|
10
|
+
return {
|
|
11
|
+
async viewLatestVersion() {
|
|
12
|
+
const { stdout } = await execFile("npm", ["view", "minecodex", "version"]);
|
|
13
|
+
return stdout.trim();
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
async createRollbackArchive(packageRoot) {
|
|
17
|
+
const directory = await mkdtemp(path.join(tempRoot, "minecodex-update-"));
|
|
18
|
+
try {
|
|
19
|
+
const { stdout } = await execFile("npm", ["pack", packageRoot, "--pack-destination", directory]);
|
|
20
|
+
const archiveName = stdout.trim().split("\n").filter(Boolean).at(-1);
|
|
21
|
+
if (!archiveName) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
|
|
22
|
+
return {
|
|
23
|
+
archivePath: path.join(directory, archiveName),
|
|
24
|
+
cleanup: () => rm(directory, { recursive: true, force: true }),
|
|
25
|
+
};
|
|
26
|
+
} catch (error) {
|
|
27
|
+
await rm(directory, { recursive: true, force: true });
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async installGlobal(spec) {
|
|
33
|
+
await execFile("npm", ["install", "-g", spec]);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async cleanup(rollback) {
|
|
37
|
+
await rollback?.cleanup?.();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export const PACKAGE_ROOT = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
"../../..",
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
export function resolvePaths({ homeDir = os.homedir() } = {}) {
|
|
11
|
+
const supportDir = path.join(homeDir, "Library", "Application Support", "MineCodex");
|
|
12
|
+
const logsDir = path.join(homeDir, "Library", "Logs", "MineCodex");
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
homeDir,
|
|
15
|
+
packageRoot: PACKAGE_ROOT,
|
|
16
|
+
featuresRoot: path.join(PACKAGE_ROOT, "features"),
|
|
17
|
+
dashboardDir: path.join(PACKAGE_ROOT, "apps", "dashboard"),
|
|
18
|
+
runtimeEntry: path.join(PACKAGE_ROOT, "packages", "runtime-host", "src", "main.mjs"),
|
|
19
|
+
supportDir,
|
|
20
|
+
logsDir,
|
|
21
|
+
configPath: path.join(supportDir, "config.json"),
|
|
22
|
+
tokenPath: path.join(supportDir, "control-token"),
|
|
23
|
+
pidPath: path.join(supportDir, "service.pid"),
|
|
24
|
+
codexPidPath: path.join(supportDir, "codex.pid"),
|
|
25
|
+
runtimeReadyPath: path.join(supportDir, "runtime-ready.json"),
|
|
26
|
+
profileDir: path.join(supportDir, "codex-profile"),
|
|
27
|
+
launchAgentPath: path.join(homeDir, "Library", "LaunchAgents", "com.vontean.minecodex.plist"),
|
|
28
|
+
serviceLogPath: path.join(logsDir, "runtime.log"),
|
|
29
|
+
serviceErrorLogPath: path.join(logsDir, "runtime-error.log"),
|
|
30
|
+
notesDataDir: path.join(homeDir, "Library", "Application Support", "CodexNotes"),
|
|
31
|
+
imagesDataDir: path.join(homeDir, ".codex-image-host"),
|
|
32
|
+
});
|
|
33
|
+
}
|