openmates 0.12.0-alpha.22 → 0.12.0-alpha.24
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/{chunk-PHFCP5AM.js → chunk-YQL7OKLK.js} +1111 -65
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +5 -1
- package/package.json +3 -2
- package/templates/caddy/core/Caddyfile +23 -0
- package/templates/caddy/preview/Caddyfile +28 -0
- package/templates/caddy/upload/Caddyfile +32 -0
- package/templates/core/docker-compose.selfhost.yml +269 -0
- package/templates/preview/docker-compose.preview.yml +48 -0
- package/templates/upload/docker-compose.yml +83 -0
|
@@ -921,8 +921,15 @@ function removeServerConfig() {
|
|
|
921
921
|
}
|
|
922
922
|
var SOURCE_COMPOSE_MARKER = join2("backend", "core", "docker-compose.yml");
|
|
923
923
|
var IMAGE_COMPOSE_MARKER = join2("backend", "core", "docker-compose.selfhost.yml");
|
|
924
|
+
var UPLOAD_COMPOSE_MARKER = join2("backend", "upload", "docker-compose.yml");
|
|
925
|
+
var PREVIEW_COMPOSE_MARKER = join2("backend", "preview", "docker-compose.preview.yml");
|
|
924
926
|
function isOpenMatesDir(dir) {
|
|
925
|
-
return
|
|
927
|
+
return [
|
|
928
|
+
SOURCE_COMPOSE_MARKER,
|
|
929
|
+
IMAGE_COMPOSE_MARKER,
|
|
930
|
+
UPLOAD_COMPOSE_MARKER,
|
|
931
|
+
PREVIEW_COMPOSE_MARKER
|
|
932
|
+
].some((marker) => existsSync3(join2(dir, marker)));
|
|
926
933
|
}
|
|
927
934
|
function resolveServerPath(flags) {
|
|
928
935
|
if (typeof flags.path === "string" && flags.path) {
|
|
@@ -2052,6 +2059,55 @@ function getClientMessagesVersionForSync(cached) {
|
|
|
2052
2059
|
if (cached.messages.length === 0) return 0;
|
|
2053
2060
|
return typeof cached.details.messages_v === "number" ? cached.details.messages_v : 0;
|
|
2054
2061
|
}
|
|
2062
|
+
var INTEREST_TAG_IDS = [
|
|
2063
|
+
"software_development",
|
|
2064
|
+
"use_the_cli",
|
|
2065
|
+
"open_source",
|
|
2066
|
+
"read_developer_docs",
|
|
2067
|
+
"run_code",
|
|
2068
|
+
"protect_my_privacy",
|
|
2069
|
+
"summarize_documents",
|
|
2070
|
+
"find_apartments",
|
|
2071
|
+
"local_life",
|
|
2072
|
+
"learn_anything"
|
|
2073
|
+
];
|
|
2074
|
+
var TOPIC_PREFERENCES_SETTINGS_KEY = "topic_preferences";
|
|
2075
|
+
function normalizeInterestTagIds(values) {
|
|
2076
|
+
const validIds = new Set(INTEREST_TAG_IDS);
|
|
2077
|
+
const normalized = [];
|
|
2078
|
+
for (const value of values) {
|
|
2079
|
+
if (!validIds.has(value)) {
|
|
2080
|
+
throw new Error(
|
|
2081
|
+
`Unknown interest tag '${value}'. Use one of: ${INTEREST_TAG_IDS.join(", ")}`
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
if (!normalized.includes(value)) {
|
|
2085
|
+
normalized.push(value);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return normalized;
|
|
2089
|
+
}
|
|
2090
|
+
function normalizeTopicPreferencesPayload(value) {
|
|
2091
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2092
|
+
return null;
|
|
2093
|
+
}
|
|
2094
|
+
const candidate = value;
|
|
2095
|
+
if (candidate.version !== 1 || !Array.isArray(candidate.selectedTagIds)) {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
const validIds = new Set(INTEREST_TAG_IDS);
|
|
2099
|
+
const selectedTagIds = [];
|
|
2100
|
+
for (const value2 of candidate.selectedTagIds) {
|
|
2101
|
+
if (validIds.has(value2) && !selectedTagIds.includes(value2)) {
|
|
2102
|
+
selectedTagIds.push(value2);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return {
|
|
2106
|
+
version: 1,
|
|
2107
|
+
selectedTagIds,
|
|
2108
|
+
updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2055
2111
|
function buildSubChatConfirmationPayload(params) {
|
|
2056
2112
|
return {
|
|
2057
2113
|
chat_id: params.chatId,
|
|
@@ -2987,6 +3043,31 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
2987
3043
|
saveSession(session);
|
|
2988
3044
|
return response.data.user ?? {};
|
|
2989
3045
|
}
|
|
3046
|
+
async getTopicPreferences() {
|
|
3047
|
+
const user = await this.whoAmI();
|
|
3048
|
+
return await this.decryptTopicPreferences(user.encrypted_settings);
|
|
3049
|
+
}
|
|
3050
|
+
async setTopicPreferences(selectedTagIds) {
|
|
3051
|
+
const user = await this.whoAmI();
|
|
3052
|
+
const settings = await this.decryptSettingsRecord(user.encrypted_settings);
|
|
3053
|
+
const payload = {
|
|
3054
|
+
version: 1,
|
|
3055
|
+
selectedTagIds: normalizeInterestTagIds(selectedTagIds),
|
|
3056
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3057
|
+
};
|
|
3058
|
+
settings[TOPIC_PREFERENCES_SETTINGS_KEY] = payload;
|
|
3059
|
+
const encryptedSettings = await encryptWithAesGcmCombined(
|
|
3060
|
+
JSON.stringify(settings),
|
|
3061
|
+
this.getMasterKeyBytes()
|
|
3062
|
+
);
|
|
3063
|
+
await this.settingsPost("topic-preferences", {
|
|
3064
|
+
encrypted_settings: encryptedSettings
|
|
3065
|
+
});
|
|
3066
|
+
return payload;
|
|
3067
|
+
}
|
|
3068
|
+
async clearTopicPreferences() {
|
|
3069
|
+
return await this.setTopicPreferences([]);
|
|
3070
|
+
}
|
|
2990
3071
|
async getLearningModeStatus() {
|
|
2991
3072
|
this.requireSession();
|
|
2992
3073
|
const response = await this.http.get(
|
|
@@ -3335,8 +3416,8 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
3335
3416
|
);
|
|
3336
3417
|
let msgEmbedIds = [];
|
|
3337
3418
|
if (clientMsgId && cache.embeds.length > 0) {
|
|
3338
|
-
const { createHash:
|
|
3339
|
-
const hashed =
|
|
3419
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
3420
|
+
const hashed = createHash6("sha256").update(clientMsgId).digest("hex");
|
|
3340
3421
|
msgEmbedIds = cache.embeds.filter(
|
|
3341
3422
|
(e) => e.hashed_message_id === hashed && // Only include parent embeds (no parent_embed_id).
|
|
3342
3423
|
// Child embeds inherit the parent's key and are loaded
|
|
@@ -3383,8 +3464,8 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
3383
3464
|
);
|
|
3384
3465
|
}
|
|
3385
3466
|
const embedId = String(embed.embed_id ?? embed.id ?? "");
|
|
3386
|
-
const { createHash:
|
|
3387
|
-
const hashedEmbedId =
|
|
3467
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
3468
|
+
const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
|
|
3388
3469
|
const embedKeyBytes = await this.resolveEmbedKey(
|
|
3389
3470
|
cache,
|
|
3390
3471
|
masterKey,
|
|
@@ -3492,7 +3573,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
3492
3573
|
async resolveEmbedKey(cache, masterKey, embed, embedId, hashedEmbedId, visited = /* @__PURE__ */ new Set()) {
|
|
3493
3574
|
if (visited.has(embedId)) return null;
|
|
3494
3575
|
visited.add(embedId);
|
|
3495
|
-
const { createHash:
|
|
3576
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
3496
3577
|
const masterKeyEntry = cache.embedKeys.find(
|
|
3497
3578
|
(ek) => ek.hashed_embed_id === hashedEmbedId && String(ek.key_type) === "master"
|
|
3498
3579
|
);
|
|
@@ -3509,7 +3590,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
3509
3590
|
if (chatKeyEntry && typeof chatKeyEntry.encrypted_embed_key === "string") {
|
|
3510
3591
|
const hashedChatId = String(chatKeyEntry.hashed_chat_id ?? "");
|
|
3511
3592
|
const owningChat = cache.chats.find((c) => {
|
|
3512
|
-
const chatHash =
|
|
3593
|
+
const chatHash = createHash6("sha256").update(String(c.details.id ?? "")).digest("hex");
|
|
3513
3594
|
return chatHash === hashedChatId;
|
|
3514
3595
|
});
|
|
3515
3596
|
if (owningChat) {
|
|
@@ -3538,7 +3619,7 @@ var OpenMatesClient = class _OpenMatesClient {
|
|
|
3538
3619
|
const parentFullId = String(
|
|
3539
3620
|
parentEmbed2.embed_id ?? parentEmbed2.id ?? ""
|
|
3540
3621
|
);
|
|
3541
|
-
const parentHashedId =
|
|
3622
|
+
const parentHashedId = createHash6("sha256").update(parentFullId).digest("hex");
|
|
3542
3623
|
const parentKey = await this.resolveEmbedKey(
|
|
3543
3624
|
cache,
|
|
3544
3625
|
masterKey,
|
|
@@ -5347,7 +5428,7 @@ Required: ${schema.required.join(", ")}`
|
|
|
5347
5428
|
const session = this.requireSession();
|
|
5348
5429
|
const masterKey = base64ToBytes(session.masterKeyExportedB64);
|
|
5349
5430
|
const cache = await this.ensureSynced();
|
|
5350
|
-
const { createHash:
|
|
5431
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
5351
5432
|
const embed = cache.embeds.find(
|
|
5352
5433
|
(e) => String(e.embed_id ?? "").startsWith(embedIdOrShort) || String(e.id ?? "").startsWith(embedIdOrShort)
|
|
5353
5434
|
);
|
|
@@ -5355,7 +5436,7 @@ Required: ${schema.required.join(", ")}`
|
|
|
5355
5436
|
throw new Error(`Embed '${embedIdOrShort}' not found in local cache.`);
|
|
5356
5437
|
}
|
|
5357
5438
|
const embedId = String(embed.embed_id ?? embed.id ?? "");
|
|
5358
|
-
const hashedEmbedId =
|
|
5439
|
+
const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
|
|
5359
5440
|
const embedKeyBytes = await this.resolveEmbedKey(
|
|
5360
5441
|
cache,
|
|
5361
5442
|
masterKey,
|
|
@@ -5415,8 +5496,8 @@ Required: ${schema.required.join(", ")}`
|
|
|
5415
5496
|
if (!embed) {
|
|
5416
5497
|
throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
|
|
5417
5498
|
}
|
|
5418
|
-
const { createHash:
|
|
5419
|
-
const hashedEmbedId =
|
|
5499
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
5500
|
+
const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
|
|
5420
5501
|
const embedKey = await this.resolveEmbedKey(
|
|
5421
5502
|
cache,
|
|
5422
5503
|
masterKey,
|
|
@@ -5510,8 +5591,8 @@ Required: ${schema.required.join(", ")}`
|
|
|
5510
5591
|
if (!embed) {
|
|
5511
5592
|
throw new Error(`Embed '${embedId}' not found in local cache. Run 'openmates chats list' to sync first.`);
|
|
5512
5593
|
}
|
|
5513
|
-
const { createHash:
|
|
5514
|
-
const hashedEmbedId =
|
|
5594
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
5595
|
+
const hashedEmbedId = createHash6("sha256").update(embedId).digest("hex");
|
|
5515
5596
|
const embedKey = await this.resolveEmbedKey(
|
|
5516
5597
|
cache,
|
|
5517
5598
|
masterKey,
|
|
@@ -5649,6 +5730,27 @@ Required: ${schema.required.join(", ")}`
|
|
|
5649
5730
|
const session = this.requireSession();
|
|
5650
5731
|
return base64ToBytes(session.masterKeyExportedB64);
|
|
5651
5732
|
}
|
|
5733
|
+
async decryptTopicPreferences(encryptedSettings) {
|
|
5734
|
+
const settings = await this.decryptSettingsRecord(encryptedSettings);
|
|
5735
|
+
return normalizeTopicPreferencesPayload(settings[TOPIC_PREFERENCES_SETTINGS_KEY]);
|
|
5736
|
+
}
|
|
5737
|
+
async decryptSettingsRecord(encryptedSettings) {
|
|
5738
|
+
if (typeof encryptedSettings !== "string" || encryptedSettings.length === 0) {
|
|
5739
|
+
return {};
|
|
5740
|
+
}
|
|
5741
|
+
const decrypted = await decryptWithAesGcmCombined(
|
|
5742
|
+
encryptedSettings,
|
|
5743
|
+
this.getMasterKeyBytes()
|
|
5744
|
+
);
|
|
5745
|
+
if (!decrypted) {
|
|
5746
|
+
throw new Error("Failed to decrypt encrypted account settings.");
|
|
5747
|
+
}
|
|
5748
|
+
const parsed = JSON.parse(decrypted);
|
|
5749
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5750
|
+
return {};
|
|
5751
|
+
}
|
|
5752
|
+
return parsed;
|
|
5753
|
+
}
|
|
5652
5754
|
getValidSessionFromDisk() {
|
|
5653
5755
|
const session = loadSession();
|
|
5654
5756
|
if (!session) return null;
|
|
@@ -7372,6 +7474,8 @@ var DIRECT_TYPES = /* @__PURE__ */ new Set([
|
|
|
7372
7474
|
"recording",
|
|
7373
7475
|
"mail-email",
|
|
7374
7476
|
"math-plot",
|
|
7477
|
+
"mindmap",
|
|
7478
|
+
"mindmaps-mindmap",
|
|
7375
7479
|
"events-event",
|
|
7376
7480
|
"health-appointment",
|
|
7377
7481
|
"shopping-product",
|
|
@@ -7399,6 +7503,8 @@ var DIRECT_TYPE_LABELS = {
|
|
|
7399
7503
|
"recording": "recording",
|
|
7400
7504
|
"mail-email": "email",
|
|
7401
7505
|
"math-plot": "plot",
|
|
7506
|
+
"mindmap": "Mind Map",
|
|
7507
|
+
"mindmaps-mindmap": "Mind Map",
|
|
7402
7508
|
"events-event": "event",
|
|
7403
7509
|
"health-appointment": "appointment",
|
|
7404
7510
|
"shopping-product": "product",
|
|
@@ -8355,6 +8461,20 @@ function renderByDirectType(embed, c, ln) {
|
|
|
8355
8461
|
ln("\x1B[2m[mathematical plot]\x1B[0m");
|
|
8356
8462
|
break;
|
|
8357
8463
|
}
|
|
8464
|
+
case "mindmap":
|
|
8465
|
+
case "mindmaps-mindmap": {
|
|
8466
|
+
const document = mindMapDocumentFromContent(c);
|
|
8467
|
+
const title = str(c.title) ?? str(document?.title) ?? "Mind Map";
|
|
8468
|
+
const nodeCount = typeof c.node_count === "number" ? c.node_count : document?.nodes.length;
|
|
8469
|
+
const edgeCount = typeof c.edge_count === "number" ? c.edge_count : document?.edges?.length ?? 0;
|
|
8470
|
+
ln(title);
|
|
8471
|
+
if (nodeCount !== void 0) ln(`\x1B[2m${nodeCount} nodes \xB7 ${edgeCount} edges\x1B[0m`);
|
|
8472
|
+
const outline = document ? mindMapOutline(document, 8) : "";
|
|
8473
|
+
if (outline) {
|
|
8474
|
+
for (const line of outline.split("\n")) ln(line);
|
|
8475
|
+
}
|
|
8476
|
+
break;
|
|
8477
|
+
}
|
|
8358
8478
|
case "images-image-result": {
|
|
8359
8479
|
const title = str(c.title) ?? "";
|
|
8360
8480
|
const source = str(c.source) ?? str(c.url) ?? "";
|
|
@@ -8520,6 +8640,24 @@ function renderDirectTypeFullscreen(embed, c) {
|
|
|
8520
8640
|
}
|
|
8521
8641
|
break;
|
|
8522
8642
|
}
|
|
8643
|
+
case "mindmap":
|
|
8644
|
+
case "mindmaps-mindmap": {
|
|
8645
|
+
const document = mindMapDocumentFromContent(c);
|
|
8646
|
+
const title = str(c.title) ?? str(document?.title) ?? "Mind Map";
|
|
8647
|
+
process.stdout.write(`\x1B[1m${title}\x1B[0m
|
|
8648
|
+
`);
|
|
8649
|
+
if (!document) {
|
|
8650
|
+
console.log("Invalid mind map JSON");
|
|
8651
|
+
break;
|
|
8652
|
+
}
|
|
8653
|
+
console.log(`${document.nodes.length} nodes \xB7 ${document.edges?.length ?? 0} edges
|
|
8654
|
+
`);
|
|
8655
|
+
console.log(mindMapOutline(document));
|
|
8656
|
+
console.log("\n```openmates_mindmap");
|
|
8657
|
+
console.log(JSON.stringify(document, null, 2));
|
|
8658
|
+
console.log("```");
|
|
8659
|
+
break;
|
|
8660
|
+
}
|
|
8523
8661
|
default: {
|
|
8524
8662
|
for (const [k, v] of Object.entries(c)) {
|
|
8525
8663
|
if (v === null || v === void 0 || k.startsWith("_")) continue;
|
|
@@ -8546,6 +8684,49 @@ function resolveResultCount(c) {
|
|
|
8546
8684
|
if (ids.length > 0) return ids.length;
|
|
8547
8685
|
return null;
|
|
8548
8686
|
}
|
|
8687
|
+
function mindMapDocumentFromContent(c) {
|
|
8688
|
+
const model = c.model;
|
|
8689
|
+
if (isMindMapDocument(model)) return model;
|
|
8690
|
+
const source = str(c.source_json);
|
|
8691
|
+
if (!source) return null;
|
|
8692
|
+
try {
|
|
8693
|
+
const parsed = JSON.parse(source);
|
|
8694
|
+
return isMindMapDocument(parsed) ? parsed : null;
|
|
8695
|
+
} catch {
|
|
8696
|
+
return null;
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8699
|
+
function isMindMapDocument(value) {
|
|
8700
|
+
if (!isRecord(value)) return false;
|
|
8701
|
+
return value.openmatesType === "mindmap" && typeof value.schemaVersion === "number" && typeof value.title === "string" && typeof value.rootId === "string" && Array.isArray(value.nodes);
|
|
8702
|
+
}
|
|
8703
|
+
function mindMapOutline(document, maxNodes = 100) {
|
|
8704
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
8705
|
+
for (const node of document.nodes) {
|
|
8706
|
+
if (typeof node.id === "string" && typeof node.label === "string") {
|
|
8707
|
+
nodesById.set(node.id, node);
|
|
8708
|
+
}
|
|
8709
|
+
}
|
|
8710
|
+
const lines = [];
|
|
8711
|
+
const visited = /* @__PURE__ */ new Set();
|
|
8712
|
+
const visit = (nodeId, depth) => {
|
|
8713
|
+
if (visited.size >= maxNodes || visited.has(nodeId)) return;
|
|
8714
|
+
const node = nodesById.get(nodeId);
|
|
8715
|
+
if (!node) return;
|
|
8716
|
+
visited.add(nodeId);
|
|
8717
|
+
lines.push(`${" ".repeat(depth)}- ${node.label}`);
|
|
8718
|
+
for (const childId of node.children ?? []) visit(childId, depth + 1);
|
|
8719
|
+
};
|
|
8720
|
+
visit(document.rootId, 0);
|
|
8721
|
+
for (const node of document.nodes) {
|
|
8722
|
+
if (visited.size >= maxNodes) break;
|
|
8723
|
+
if (!visited.has(node.id)) visit(node.id, 0);
|
|
8724
|
+
}
|
|
8725
|
+
return lines.join("\n");
|
|
8726
|
+
}
|
|
8727
|
+
function isRecord(value) {
|
|
8728
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8729
|
+
}
|
|
8549
8730
|
async function resolveChildResults(c, client) {
|
|
8550
8731
|
const inline = c.results;
|
|
8551
8732
|
if (Array.isArray(inline) && inline.length > 0) {
|
|
@@ -8574,14 +8755,211 @@ function formatTs(ts) {
|
|
|
8574
8755
|
|
|
8575
8756
|
// src/server.ts
|
|
8576
8757
|
import { execSync, spawn as nodeSpawn } from "child_process";
|
|
8577
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
8578
|
-
import { copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8758
|
+
import { createHash as createHash5, randomBytes as randomBytes2 } from "crypto";
|
|
8759
|
+
import { chmodSync as chmodSync2, copyFileSync, cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, readFileSync as readFileSync5, readdirSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
8579
8760
|
import { createInterface as createInterface2 } from "readline";
|
|
8580
8761
|
import { createInterface as createPromptInterface } from "readline/promises";
|
|
8581
8762
|
import { homedir as homedir5 } from "os";
|
|
8582
8763
|
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
8764
|
+
|
|
8765
|
+
// src/serverPlanning.ts
|
|
8766
|
+
var CORE_WORKER_SERVICES = [
|
|
8767
|
+
"task-worker",
|
|
8768
|
+
"task-scheduler",
|
|
8769
|
+
"app-ai-worker",
|
|
8770
|
+
"app-images-worker",
|
|
8771
|
+
"app-music-worker",
|
|
8772
|
+
"app-videos-worker",
|
|
8773
|
+
"app-pdf-worker",
|
|
8774
|
+
"app-docs-worker",
|
|
8775
|
+
"app-code-worker",
|
|
8776
|
+
"app-social-media-worker"
|
|
8777
|
+
];
|
|
8778
|
+
var CORE_OBSERVABILITY_BY_PROFILE = {
|
|
8779
|
+
minimal: [],
|
|
8780
|
+
standard: ["openobserve", "promtail"],
|
|
8781
|
+
production: ["openobserve", "promtail", "prometheus", "cadvisor"]
|
|
8782
|
+
};
|
|
8783
|
+
var ROLE_DEFINITIONS = {
|
|
8784
|
+
core: {
|
|
8785
|
+
dataBearing: true,
|
|
8786
|
+
requiredServices: ["api", "cms", "cms-database", "cache", "vault", "vault-setup", "cms-setup"],
|
|
8787
|
+
optionalServices: [...CORE_WORKER_SERVICES, "admin-sidecar", "webapp", "openobserve", "promtail", "prometheus", "cadvisor", "alertmanager"],
|
|
8788
|
+
healthChecks: ["http://localhost:8000/health"],
|
|
8789
|
+
templatePath: "templates/core/docker-compose.selfhost.yml",
|
|
8790
|
+
composeFile: "backend/core/docker-compose.selfhost.yml"
|
|
8791
|
+
},
|
|
8792
|
+
upload: {
|
|
8793
|
+
dataBearing: true,
|
|
8794
|
+
requiredServices: ["app-uploads", "clamav", "vault", "vault-setup", "admin-sidecar"],
|
|
8795
|
+
optionalServices: [],
|
|
8796
|
+
healthChecks: ["http://localhost:8000/health"],
|
|
8797
|
+
templatePath: "templates/upload/docker-compose.yml",
|
|
8798
|
+
composeFile: "backend/upload/docker-compose.yml"
|
|
8799
|
+
},
|
|
8800
|
+
preview: {
|
|
8801
|
+
dataBearing: false,
|
|
8802
|
+
requiredServices: ["preview", "admin-sidecar"],
|
|
8803
|
+
optionalServices: ["cache"],
|
|
8804
|
+
healthChecks: ["http://localhost:8080/health"],
|
|
8805
|
+
templatePath: "templates/preview/docker-compose.preview.yml",
|
|
8806
|
+
composeFile: "backend/preview/docker-compose.preview.yml"
|
|
8807
|
+
}
|
|
8808
|
+
};
|
|
8809
|
+
function unique(items) {
|
|
8810
|
+
return [...new Set(items.map((item) => item.trim()).filter(Boolean))];
|
|
8811
|
+
}
|
|
8812
|
+
function csv(value) {
|
|
8813
|
+
if (value === void 0) return [];
|
|
8814
|
+
if (Array.isArray(value)) return unique(value.flatMap((item) => item.split(",")));
|
|
8815
|
+
return unique(value.split(","));
|
|
8816
|
+
}
|
|
8817
|
+
function parseServerRole(value) {
|
|
8818
|
+
if (!value) return "core";
|
|
8819
|
+
if (value === "core" || value === "upload" || value === "preview") return value;
|
|
8820
|
+
throw new Error(`Unsupported server role '${value}'. Use core, upload, or preview.`);
|
|
8821
|
+
}
|
|
8822
|
+
function planServerRuntime(input) {
|
|
8823
|
+
const role = parseServerRole(input.role);
|
|
8824
|
+
const definition = ROLE_DEFINITIONS[role];
|
|
8825
|
+
const coreProfile = input.profile ?? "production";
|
|
8826
|
+
const profile = role === "core" ? coreProfile : null;
|
|
8827
|
+
const profileServices = role === "core" ? [...CORE_OBSERVABILITY_BY_PROFILE[coreProfile]] : [];
|
|
8828
|
+
if (role === "core" && input.withAlerts) profileServices.push("alertmanager");
|
|
8829
|
+
const defaultServices = role === "core" ? unique([...definition.requiredServices, ...CORE_WORKER_SERVICES, "admin-sidecar", ...profileServices, "webapp"]) : unique([...definition.requiredServices, ...definition.optionalServices]);
|
|
8830
|
+
return {
|
|
8831
|
+
role,
|
|
8832
|
+
profile,
|
|
8833
|
+
dataBearing: definition.dataBearing,
|
|
8834
|
+
composeFiles: [definition.composeFile],
|
|
8835
|
+
requiredServices: [...definition.requiredServices],
|
|
8836
|
+
profileServices,
|
|
8837
|
+
defaultServices,
|
|
8838
|
+
healthChecks: [...definition.healthChecks]
|
|
8839
|
+
};
|
|
8840
|
+
}
|
|
8841
|
+
function resolveServiceSelection(roleValue, filter = {}) {
|
|
8842
|
+
const role = parseServerRole(roleValue);
|
|
8843
|
+
const definition = ROLE_DEFINITIONS[role];
|
|
8844
|
+
const allowed = /* @__PURE__ */ new Set([...definition.requiredServices, ...definition.optionalServices]);
|
|
8845
|
+
const requested = csv(filter.services);
|
|
8846
|
+
const excluded = new Set(csv(filter.exclude));
|
|
8847
|
+
const base = requested.length ? requested : [...allowed];
|
|
8848
|
+
for (const service of [...base, ...excluded]) {
|
|
8849
|
+
if (!allowed.has(service)) {
|
|
8850
|
+
throw new Error(`Invalid service '${service}' for ${role} role.`);
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
return base.filter((service) => !excluded.has(service));
|
|
8854
|
+
}
|
|
8855
|
+
function planUpdate(input) {
|
|
8856
|
+
const runtime = planServerRuntime({ role: input.role });
|
|
8857
|
+
const selectedServices = input.selectedServices?.length ? input.selectedServices : runtime.defaultServices;
|
|
8858
|
+
const missingRequiredSecrets = input.missingRequiredSecrets ?? [];
|
|
8859
|
+
const blocked = input.continuous === true && missingRequiredSecrets.length > 0;
|
|
8860
|
+
const steps = ["preflight"];
|
|
8861
|
+
const backupName = runtime.dataBearing && input.skipBackup !== true ? `latest-pre-update-${runtime.role}.tar.zst` : null;
|
|
8862
|
+
if (backupName) steps.push("backup:latest-pre-update");
|
|
8863
|
+
steps.push("pull", "up", "health-check");
|
|
8864
|
+
return {
|
|
8865
|
+
role: runtime.role,
|
|
8866
|
+
selectedServices,
|
|
8867
|
+
steps,
|
|
8868
|
+
commands: [
|
|
8869
|
+
`docker compose pull ${selectedServices.join(" ")}`,
|
|
8870
|
+
`docker compose up -d ${selectedServices.join(" ")}`
|
|
8871
|
+
],
|
|
8872
|
+
backupName,
|
|
8873
|
+
blocked,
|
|
8874
|
+
blockReason: blocked ? `Blocked by missing required secrets: ${missingRequiredSecrets.join(", ")}` : null
|
|
8875
|
+
};
|
|
8876
|
+
}
|
|
8877
|
+
function planBackup(input) {
|
|
8878
|
+
const role = parseServerRole(input.role);
|
|
8879
|
+
const contentsByRole = {
|
|
8880
|
+
core: ["postgres-dump", "directus-uploads", "directus-extensions", "vault-data", "vault-setup-data", "runtime-env", "runtime-config", "manifest", "checksums"],
|
|
8881
|
+
upload: ["vault-data", "vault-setup-data", "runtime-env", "runtime-config", "manifest", "checksums"],
|
|
8882
|
+
preview: ["runtime-env", "runtime-config", "preview-cache", "manifest", "checksums"]
|
|
8883
|
+
};
|
|
8884
|
+
const contents = [...contentsByRole[role]];
|
|
8885
|
+
if (input.includeObservability) contents.push("openobserve-data", "prometheus-data");
|
|
8886
|
+
return { role, contents, fileMode: 384 };
|
|
8887
|
+
}
|
|
8888
|
+
function planRestore(input) {
|
|
8889
|
+
const role = parseServerRole(input.role);
|
|
8890
|
+
return {
|
|
8891
|
+
role,
|
|
8892
|
+
file: input.file,
|
|
8893
|
+
requiresConfirmation: input.yes !== true,
|
|
8894
|
+
steps: input.yes === true ? ["stop", "restore", "start", "health-check"] : ["confirm", "stop", "restore", "start", "health-check"]
|
|
8895
|
+
};
|
|
8896
|
+
}
|
|
8897
|
+
function planCaddyCommand(input) {
|
|
8898
|
+
const role = parseServerRole(input.role);
|
|
8899
|
+
const templatePath = `templates/caddy/${role}/Caddyfile`;
|
|
8900
|
+
const stepsByAction = {
|
|
8901
|
+
check: ["render-template", "validate"],
|
|
8902
|
+
status: ["hash-template", "hash-applied", "validate"],
|
|
8903
|
+
diff: ["hash-template", "hash-applied", "diff"],
|
|
8904
|
+
apply: ["render-template", "validate", "backup-applied", "write", "reload"]
|
|
8905
|
+
};
|
|
8906
|
+
return {
|
|
8907
|
+
role,
|
|
8908
|
+
action: input.action,
|
|
8909
|
+
templatePath,
|
|
8910
|
+
appliedPath: input.appliedPath ?? "/etc/caddy/Caddyfile",
|
|
8911
|
+
steps: stepsByAction[input.action]
|
|
8912
|
+
};
|
|
8913
|
+
}
|
|
8914
|
+
function planContinuousUpdateService(input) {
|
|
8915
|
+
const role = parseServerRole(input.role);
|
|
8916
|
+
const channel = input.channel ?? "main";
|
|
8917
|
+
const window = input.window ?? "02:00-04:00 UTC";
|
|
8918
|
+
const serviceName = `openmates-${role}-continuous-update.service`;
|
|
8919
|
+
const timerName = `openmates-${role}-continuous-update.timer`;
|
|
8920
|
+
return {
|
|
8921
|
+
role,
|
|
8922
|
+
serviceName,
|
|
8923
|
+
timerName,
|
|
8924
|
+
unit: [
|
|
8925
|
+
"[Unit]",
|
|
8926
|
+
`Description=OpenMates ${role} continuous updater`,
|
|
8927
|
+
"After=docker.service network-online.target",
|
|
8928
|
+
"Wants=network-online.target",
|
|
8929
|
+
"",
|
|
8930
|
+
"[Service]",
|
|
8931
|
+
"Type=oneshot",
|
|
8932
|
+
`ExecStart=openmates server update --role ${role} --channel ${channel} --continuous`,
|
|
8933
|
+
`Environment=OPENMATES_UPDATE_WINDOW=${window}`,
|
|
8934
|
+
""
|
|
8935
|
+
].join("\n"),
|
|
8936
|
+
timer: [
|
|
8937
|
+
"[Unit]",
|
|
8938
|
+
`Description=Run OpenMates ${role} continuous updater`,
|
|
8939
|
+
"",
|
|
8940
|
+
"[Timer]",
|
|
8941
|
+
"OnCalendar=*:0/30",
|
|
8942
|
+
"Persistent=true",
|
|
8943
|
+
"",
|
|
8944
|
+
"[Install]",
|
|
8945
|
+
"WantedBy=timers.target",
|
|
8946
|
+
""
|
|
8947
|
+
].join("\n")
|
|
8948
|
+
};
|
|
8949
|
+
}
|
|
8950
|
+
|
|
8951
|
+
// src/server.ts
|
|
8583
8952
|
var SOURCE_COMPOSE_FILE = join3("backend", "core", "docker-compose.yml");
|
|
8584
|
-
var
|
|
8953
|
+
var ROLE_IMAGE_COMPOSE_FILES = {
|
|
8954
|
+
core: join3("backend", "core", "docker-compose.selfhost.yml"),
|
|
8955
|
+
upload: join3("backend", "upload", "docker-compose.yml"),
|
|
8956
|
+
preview: join3("backend", "preview", "docker-compose.preview.yml")
|
|
8957
|
+
};
|
|
8958
|
+
var ROLE_TEMPLATE_FILES = {
|
|
8959
|
+
core: join3("core", "docker-compose.selfhost.yml"),
|
|
8960
|
+
upload: join3("upload", "docker-compose.yml"),
|
|
8961
|
+
preview: join3("preview", "docker-compose.preview.yml")
|
|
8962
|
+
};
|
|
8585
8963
|
var COMPOSE_OVERRIDE = join3("backend", "core", "docker-compose.override.yml");
|
|
8586
8964
|
var DEFAULT_INSTALL_PATH = join3(homedir5(), "openmates");
|
|
8587
8965
|
var REPO_URL = "https://github.com/glowingkitty/OpenMates.git";
|
|
@@ -8668,6 +9046,12 @@ SELF_HOST_SIGNUP_MODE=invite_only
|
|
|
8668
9046
|
SELF_HOST_SIGNUP_ALLOWED_DOMAINS=
|
|
8669
9047
|
SELF_HOST_FIRST_INVITE_CODE=
|
|
8670
9048
|
APPLICATION_PREVIEW_ORIGIN=
|
|
9049
|
+
PROD_CORE_API_URL=
|
|
9050
|
+
PROD_INTERNAL_API_SHARED_TOKEN=
|
|
9051
|
+
DEV_CORE_API_URL=
|
|
9052
|
+
DEV_INTERNAL_API_SHARED_TOKEN=
|
|
9053
|
+
PREVIEW_CORS_ORIGINS=https://openmates.org
|
|
9054
|
+
PREVIEW_ALLOWED_REFERERS=https://openmates.org/*
|
|
8671
9055
|
OPENMATES_IMAGE_REGISTRY=${DEFAULT_IMAGE_REGISTRY}
|
|
8672
9056
|
OPENMATES_IMAGE_TAG=
|
|
8673
9057
|
GIT_WORK_DIR=
|
|
@@ -8714,9 +9098,26 @@ function loadConfigForInstallPath(installPath) {
|
|
|
8714
9098
|
}
|
|
8715
9099
|
function getInstallMode(installPath, config = loadConfigForInstallPath(installPath)) {
|
|
8716
9100
|
if (config?.installMode) return config.installMode;
|
|
8717
|
-
if (existsSync5(join3(installPath,
|
|
9101
|
+
if (Object.values(ROLE_IMAGE_COMPOSE_FILES).some((composeFile) => existsSync5(join3(installPath, composeFile)))) return "image";
|
|
8718
9102
|
return "source";
|
|
8719
9103
|
}
|
|
9104
|
+
function getServerRole(flags, config) {
|
|
9105
|
+
return parseServerRole(typeof flags.role === "string" ? flags.role : config?.serverRole);
|
|
9106
|
+
}
|
|
9107
|
+
function getCoreProfile(flags, config) {
|
|
9108
|
+
const value = typeof flags.profile === "string" ? flags.profile : config?.serverProfile;
|
|
9109
|
+
if (value === "minimal" || value === "standard" || value === "production") return value;
|
|
9110
|
+
return "production";
|
|
9111
|
+
}
|
|
9112
|
+
function hasServiceFilter(flags) {
|
|
9113
|
+
return typeof flags.services === "string" || typeof flags.exclude === "string";
|
|
9114
|
+
}
|
|
9115
|
+
function selectedComposeServices(role, flags) {
|
|
9116
|
+
return resolveServiceSelection(role, {
|
|
9117
|
+
services: typeof flags.services === "string" ? flags.services : void 0,
|
|
9118
|
+
exclude: typeof flags.exclude === "string" ? flags.exclude : void 0
|
|
9119
|
+
});
|
|
9120
|
+
}
|
|
8720
9121
|
function shouldPullImages() {
|
|
8721
9122
|
return process.env.OPENMATES_SKIP_IMAGE_PULL !== "1";
|
|
8722
9123
|
}
|
|
@@ -8779,8 +9180,8 @@ ${renderFeatureOverrides(overrides)}`;
|
|
|
8779
9180
|
function featureKind(featureId) {
|
|
8780
9181
|
return featureId.split(":", 1)[0] || "unknown";
|
|
8781
9182
|
}
|
|
8782
|
-
function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath)) {
|
|
8783
|
-
const composeFile = installMode === "image" ?
|
|
9183
|
+
function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath), role = "core") {
|
|
9184
|
+
const composeFile = installMode === "image" ? ROLE_IMAGE_COMPOSE_FILES[role] : SOURCE_COMPOSE_FILE;
|
|
8784
9185
|
const args = ["compose", "--env-file", ".env", "-f", composeFile];
|
|
8785
9186
|
if (withOverrides && existsSync5(join3(installPath, COMPOSE_OVERRIDE))) {
|
|
8786
9187
|
args.push("-f", COMPOSE_OVERRIDE);
|
|
@@ -8912,25 +9313,35 @@ async function fetchText(url) {
|
|
|
8912
9313
|
}
|
|
8913
9314
|
return response.text();
|
|
8914
9315
|
}
|
|
8915
|
-
|
|
9316
|
+
function packagedTemplatePath(role) {
|
|
9317
|
+
return join3(dirname(new URL(import.meta.url).pathname), "..", "templates", ROLE_TEMPLATE_FILES[role]);
|
|
9318
|
+
}
|
|
9319
|
+
function packagedCaddyTemplatePath(role) {
|
|
9320
|
+
return join3(dirname(new URL(import.meta.url).pathname), "..", "templates", "caddy", role, "Caddyfile");
|
|
9321
|
+
}
|
|
9322
|
+
function fileHash(path) {
|
|
9323
|
+
if (!existsSync5(path)) return null;
|
|
9324
|
+
return createHash5("sha256").update(readFileSync5(path)).digest("hex");
|
|
9325
|
+
}
|
|
9326
|
+
async function loadSelfHostComposeTemplate(templateRef, role) {
|
|
8916
9327
|
const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
|
|
8917
9328
|
if (templateDir) {
|
|
8918
|
-
return readFileSync5(join3(resolve3(templateDir),
|
|
9329
|
+
return readFileSync5(join3(resolve3(templateDir), ROLE_TEMPLATE_FILES[role]), "utf-8");
|
|
8919
9330
|
}
|
|
8920
9331
|
const overrideUrl = process.env.OPENMATES_SELFHOST_COMPOSE_URL;
|
|
8921
9332
|
if (overrideUrl) {
|
|
8922
9333
|
return fetchText(overrideUrl);
|
|
8923
9334
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
);
|
|
9335
|
+
const packaged = packagedTemplatePath(role);
|
|
9336
|
+
if (existsSync5(packaged)) return readFileSync5(packaged, "utf-8");
|
|
9337
|
+
return fetchText(`https://raw.githubusercontent.com/glowingkitty/OpenMates/${templateRef}/${ROLE_IMAGE_COMPOSE_FILES[role]}`);
|
|
8927
9338
|
}
|
|
8928
|
-
async function writeImageModeRuntimeFiles(installPath, imageTag) {
|
|
8929
|
-
const
|
|
8930
|
-
const vaultConfigDir = join3(
|
|
9339
|
+
async function writeImageModeRuntimeFiles(installPath, imageTag, role) {
|
|
9340
|
+
const roleDir = join3(installPath, "backend", role === "core" ? "core" : role);
|
|
9341
|
+
const vaultConfigDir = join3(roleDir, "vault", "config");
|
|
8931
9342
|
mkdirSync3(vaultConfigDir, { recursive: true });
|
|
8932
9343
|
mkdirSync3(join3(installPath, "config", "providers"), { recursive: true });
|
|
8933
|
-
writeFileSync3(join3(
|
|
9344
|
+
writeFileSync3(join3(installPath, ROLE_IMAGE_COMPOSE_FILES[role]), await loadSelfHostComposeTemplate(templateRefForImageTag(imageTag, getPackageVersion()), role));
|
|
8934
9345
|
writeFileSync3(join3(vaultConfigDir, "vault.hcl"), VAULT_CONFIG_TEMPLATE);
|
|
8935
9346
|
ensureImageRuntimeConfig(installPath);
|
|
8936
9347
|
const envPath = join3(installPath, ".env");
|
|
@@ -8975,7 +9386,16 @@ async function checkUrl(url) {
|
|
|
8975
9386
|
clearTimeout(timeout);
|
|
8976
9387
|
}
|
|
8977
9388
|
}
|
|
8978
|
-
async function waitForServerHealth(installPath) {
|
|
9389
|
+
async function waitForServerHealth(installPath, role = "core") {
|
|
9390
|
+
if (role === "upload" || role === "preview") {
|
|
9391
|
+
const healthUrl = role === "upload" ? "http://localhost:8000/health" : "http://localhost:8080/health";
|
|
9392
|
+
const deadline2 = Date.now() + UPDATE_HEALTH_TIMEOUT_MS;
|
|
9393
|
+
while (Date.now() < deadline2) {
|
|
9394
|
+
if (await checkUrl(healthUrl)) return;
|
|
9395
|
+
await sleep2(UPDATE_HEALTH_INTERVAL_MS);
|
|
9396
|
+
}
|
|
9397
|
+
throw new Error(`Updated ${role} server did not pass health checks in time. Tried ${healthUrl}.`);
|
|
9398
|
+
}
|
|
8979
9399
|
const envPath = join3(installPath, ".env");
|
|
8980
9400
|
const envContent = existsSync5(envPath) ? readFileSync5(envPath, "utf-8") : "";
|
|
8981
9401
|
const urls = deriveSelfHostCliUrls(envContent);
|
|
@@ -9104,6 +9524,159 @@ function boolFromFlag(value, defaultValue = false) {
|
|
|
9104
9524
|
const normalized = value.toLowerCase();
|
|
9105
9525
|
return ["1", "true", "yes", "y", "on"].includes(normalized);
|
|
9106
9526
|
}
|
|
9527
|
+
function shellQuote(value) {
|
|
9528
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
9529
|
+
}
|
|
9530
|
+
function nowStamp() {
|
|
9531
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9532
|
+
}
|
|
9533
|
+
function backupRoot(installPath) {
|
|
9534
|
+
return join3(installPath, "backups");
|
|
9535
|
+
}
|
|
9536
|
+
function roleBackupDir(installPath, role) {
|
|
9537
|
+
return join3(backupRoot(installPath), role);
|
|
9538
|
+
}
|
|
9539
|
+
function updateStatusFile(installPath, role) {
|
|
9540
|
+
return join3(installPath, ".openmates", `${role}-update-status.json`);
|
|
9541
|
+
}
|
|
9542
|
+
function writeUpdateStatus(installPath, role, status) {
|
|
9543
|
+
const filePath = updateStatusFile(installPath, role);
|
|
9544
|
+
mkdirSync3(dirname(filePath), { recursive: true, mode: 448 });
|
|
9545
|
+
writeFileSync3(filePath, `${JSON.stringify({ role, updated_at: (/* @__PURE__ */ new Date()).toISOString(), ...status }, null, 2)}
|
|
9546
|
+
`, { mode: 384 });
|
|
9547
|
+
}
|
|
9548
|
+
function copyIfExists(source, destination) {
|
|
9549
|
+
if (!existsSync5(source)) return;
|
|
9550
|
+
mkdirSync3(dirname(destination), { recursive: true });
|
|
9551
|
+
cpSync(source, destination, { recursive: true, force: true });
|
|
9552
|
+
}
|
|
9553
|
+
function readEnvMap(installPath) {
|
|
9554
|
+
const envPath = join3(installPath, ".env");
|
|
9555
|
+
if (!existsSync5(envPath)) return {};
|
|
9556
|
+
const values = {};
|
|
9557
|
+
for (const line of readFileSync5(envPath, "utf-8").split("\n")) {
|
|
9558
|
+
const trimmed = line.trim();
|
|
9559
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
9560
|
+
const eqIdx = trimmed.indexOf("=");
|
|
9561
|
+
if (eqIdx === -1) continue;
|
|
9562
|
+
values[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1).replace(/^"|"$/g, "");
|
|
9563
|
+
}
|
|
9564
|
+
return values;
|
|
9565
|
+
}
|
|
9566
|
+
function requiredRuntimeEnvKeys(role) {
|
|
9567
|
+
if (role === "core") {
|
|
9568
|
+
return [
|
|
9569
|
+
"DATABASE_ADMIN_EMAIL",
|
|
9570
|
+
"DATABASE_ADMIN_PASSWORD",
|
|
9571
|
+
"DATABASE_NAME",
|
|
9572
|
+
"DATABASE_USERNAME",
|
|
9573
|
+
"DATABASE_PASSWORD",
|
|
9574
|
+
"DIRECTUS_TOKEN",
|
|
9575
|
+
"DIRECTUS_SECRET",
|
|
9576
|
+
"DRAGONFLY_PASSWORD",
|
|
9577
|
+
"INTERNAL_API_SHARED_TOKEN"
|
|
9578
|
+
];
|
|
9579
|
+
}
|
|
9580
|
+
if (role === "upload") {
|
|
9581
|
+
return ["PROD_CORE_API_URL", "PROD_INTERNAL_API_SHARED_TOKEN", "DEV_CORE_API_URL", "DEV_INTERNAL_API_SHARED_TOKEN"];
|
|
9582
|
+
}
|
|
9583
|
+
return ["PREVIEW_CORS_ORIGINS", "PREVIEW_ALLOWED_REFERERS"];
|
|
9584
|
+
}
|
|
9585
|
+
function missingRequiredEnvKeys(installPath, role) {
|
|
9586
|
+
const env = readEnvMap(installPath);
|
|
9587
|
+
return requiredRuntimeEnvKeys(role).filter((key) => !env[key]);
|
|
9588
|
+
}
|
|
9589
|
+
function writeChecksums(rootDir) {
|
|
9590
|
+
const lines = [];
|
|
9591
|
+
const walk = (dir) => {
|
|
9592
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
9593
|
+
const path = join3(dir, entry.name);
|
|
9594
|
+
if (entry.isDirectory()) {
|
|
9595
|
+
walk(path);
|
|
9596
|
+
continue;
|
|
9597
|
+
}
|
|
9598
|
+
if (entry.name === "checksums.sha256") continue;
|
|
9599
|
+
const relative2 = path.slice(rootDir.length + 1);
|
|
9600
|
+
const hash = createHash5("sha256").update(readFileSync5(path)).digest("hex");
|
|
9601
|
+
lines.push(`${hash} ${relative2}`);
|
|
9602
|
+
}
|
|
9603
|
+
};
|
|
9604
|
+
walk(rootDir);
|
|
9605
|
+
writeFileSync3(join3(rootDir, "checksums.sha256"), `${lines.sort().join("\n")}
|
|
9606
|
+
`);
|
|
9607
|
+
}
|
|
9608
|
+
function createServerBackup(installPath, role, options = {}) {
|
|
9609
|
+
const plan = planBackup({ role, includeObservability: options.includeObservability });
|
|
9610
|
+
const backupDir = roleBackupDir(installPath, role);
|
|
9611
|
+
mkdirSync3(backupDir, { recursive: true, mode: 448 });
|
|
9612
|
+
const archivePath = options.output ? resolve3(options.output) : join3(backupDir, options.preUpdate ? `latest-pre-update-${role}.tar.gz` : `openmates-${role}-${nowStamp()}.tar.gz`);
|
|
9613
|
+
const tempDir = mkdtempSync(join3(backupDir, ".tmp-"));
|
|
9614
|
+
const env = readEnvMap(installPath);
|
|
9615
|
+
const manifest = {
|
|
9616
|
+
role,
|
|
9617
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9618
|
+
cli_version: getPackageVersion(),
|
|
9619
|
+
image_tag: getEnvVar(existsSync5(join3(installPath, ".env")) ? readFileSync5(join3(installPath, ".env"), "utf-8") : "", "OPENMATES_IMAGE_TAG"),
|
|
9620
|
+
include_observability: options.includeObservability === true,
|
|
9621
|
+
contents: plan.contents
|
|
9622
|
+
};
|
|
9623
|
+
try {
|
|
9624
|
+
writeFileSync3(join3(tempDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
9625
|
+
`);
|
|
9626
|
+
copyIfExists(join3(installPath, ".env"), join3(tempDir, "runtime", ".env"));
|
|
9627
|
+
copyIfExists(join3(installPath, "config"), join3(tempDir, "runtime", "config"));
|
|
9628
|
+
if (role === "core") {
|
|
9629
|
+
const databaseUser = env.DATABASE_USERNAME || "directus";
|
|
9630
|
+
const databaseName = env.DATABASE_NAME || "directus";
|
|
9631
|
+
try {
|
|
9632
|
+
const dump = execSync(`docker exec cms-database pg_dump -U ${shellQuote(databaseUser)} ${shellQuote(databaseName)}`, { encoding: "utf-8" });
|
|
9633
|
+
writeFileSync3(join3(tempDir, "postgres.sql"), dump);
|
|
9634
|
+
} catch (error) {
|
|
9635
|
+
throw new Error(`Postgres backup failed. Is cms-database running? ${error instanceof Error ? error.message : String(error)}`);
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
for (const item of [
|
|
9639
|
+
[join3(installPath, "backend", "core", "uploads"), join3(tempDir, "directus-uploads")],
|
|
9640
|
+
[join3(installPath, "backend", "core", "extensions"), join3(tempDir, "directus-extensions")],
|
|
9641
|
+
[join3(installPath, "backend", role, "vault"), join3(tempDir, `${role}-vault-config`)]
|
|
9642
|
+
]) {
|
|
9643
|
+
copyIfExists(item[0], item[1]);
|
|
9644
|
+
}
|
|
9645
|
+
writeChecksums(tempDir);
|
|
9646
|
+
execSync(`tar -czf ${shellQuote(archivePath)} -C ${shellQuote(tempDir)} .`, { stdio: "pipe" });
|
|
9647
|
+
chmodSync2(archivePath, plan.fileMode);
|
|
9648
|
+
return archivePath;
|
|
9649
|
+
} finally {
|
|
9650
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
9651
|
+
}
|
|
9652
|
+
}
|
|
9653
|
+
function restoreServerBackup(installPath, role, file) {
|
|
9654
|
+
const archivePath = resolve3(file);
|
|
9655
|
+
if (!existsSync5(archivePath)) throw new Error(`Backup file not found: ${archivePath}`);
|
|
9656
|
+
mkdirSync3(roleBackupDir(installPath, role), { recursive: true, mode: 448 });
|
|
9657
|
+
const tempDir = mkdtempSync(join3(roleBackupDir(installPath, role), ".restore-"));
|
|
9658
|
+
const env = readEnvMap(installPath);
|
|
9659
|
+
try {
|
|
9660
|
+
execSync(`tar -xzf ${shellQuote(archivePath)} -C ${shellQuote(tempDir)}`, { stdio: "pipe" });
|
|
9661
|
+
const manifestPath = join3(tempDir, "manifest.json");
|
|
9662
|
+
if (!existsSync5(manifestPath)) throw new Error("Backup archive is missing manifest.json.");
|
|
9663
|
+
const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
9664
|
+
if (manifest.role !== role) throw new Error(`Backup role '${manifest.role}' does not match requested role '${role}'.`);
|
|
9665
|
+
copyIfExists(join3(tempDir, "runtime", ".env"), join3(installPath, ".env"));
|
|
9666
|
+
copyIfExists(join3(tempDir, "runtime", "config"), join3(installPath, "config"));
|
|
9667
|
+
const postgresDump = join3(tempDir, "postgres.sql");
|
|
9668
|
+
if (role === "core" && existsSync5(postgresDump)) {
|
|
9669
|
+
const databaseUser = env.DATABASE_USERNAME || "directus";
|
|
9670
|
+
const databaseName = env.DATABASE_NAME || "directus";
|
|
9671
|
+
execSync(`docker exec -i cms-database psql -U ${shellQuote(databaseUser)} ${shellQuote(databaseName)}`, {
|
|
9672
|
+
input: readFileSync5(postgresDump),
|
|
9673
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9674
|
+
});
|
|
9675
|
+
}
|
|
9676
|
+
} finally {
|
|
9677
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9107
9680
|
async function promptText(question, defaultValue = "") {
|
|
9108
9681
|
const rl = createPromptInterface({ input: process.stdin, output: process.stderr });
|
|
9109
9682
|
try {
|
|
@@ -9200,11 +9773,13 @@ async function serverStatus(flags) {
|
|
|
9200
9773
|
const installPath = resolveServerPath(flags);
|
|
9201
9774
|
ensureGitWorkDirEnv(installPath);
|
|
9202
9775
|
const config = loadConfigForInstallPath(installPath);
|
|
9776
|
+
const role = getServerRole(flags, config);
|
|
9203
9777
|
const withOverrides = config?.composeProfile === "full";
|
|
9204
|
-
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "ps"];
|
|
9778
|
+
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "ps"];
|
|
9205
9779
|
if (flags.json === true) {
|
|
9206
9780
|
args.push("--format", "json");
|
|
9207
9781
|
}
|
|
9782
|
+
if (hasServiceFilter(flags)) args.push(...selectedComposeServices(role, flags));
|
|
9208
9783
|
const code = await runInteractive("docker", args, installPath);
|
|
9209
9784
|
if (code !== 0) process.exit(code);
|
|
9210
9785
|
}
|
|
@@ -9215,9 +9790,15 @@ async function serverStart(flags) {
|
|
|
9215
9790
|
warnIfMissingLlmCredentials(installPath);
|
|
9216
9791
|
const withOverrides = flags["with-overrides"] === true;
|
|
9217
9792
|
const config = loadConfigForInstallPath(installPath);
|
|
9793
|
+
const role = getServerRole(flags, config);
|
|
9218
9794
|
const installMode = getInstallMode(installPath, config);
|
|
9219
|
-
const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
|
|
9220
|
-
const args = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
9795
|
+
const pullArgs = [...composeArgs(installPath, withOverrides, installMode, role), "pull"];
|
|
9796
|
+
const args = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9797
|
+
if (hasServiceFilter(flags)) {
|
|
9798
|
+
const services = selectedComposeServices(role, flags);
|
|
9799
|
+
pullArgs.push(...services);
|
|
9800
|
+
args.push(...services);
|
|
9801
|
+
}
|
|
9221
9802
|
if (config && withOverrides && config.composeProfile !== "full") {
|
|
9222
9803
|
saveServerConfig({ ...config, composeProfile: "full" });
|
|
9223
9804
|
}
|
|
@@ -9246,8 +9827,9 @@ async function serverStop(flags) {
|
|
|
9246
9827
|
const installPath = resolveServerPath(flags);
|
|
9247
9828
|
ensureGitWorkDirEnv(installPath);
|
|
9248
9829
|
const config = loadConfigForInstallPath(installPath);
|
|
9830
|
+
const role = getServerRole(flags, config);
|
|
9249
9831
|
const withOverrides = config?.composeProfile === "full";
|
|
9250
|
-
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "down"];
|
|
9832
|
+
const args = hasServiceFilter(flags) ? [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "stop", ...selectedComposeServices(role, flags)] : [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "down"];
|
|
9251
9833
|
console.error("Stopping OpenMates server...");
|
|
9252
9834
|
const code = await runInteractive("docker", args, installPath);
|
|
9253
9835
|
if (code !== 0) process.exit(code);
|
|
@@ -9262,31 +9844,36 @@ async function serverRestart(flags) {
|
|
|
9262
9844
|
const installPath = resolveServerPath(flags);
|
|
9263
9845
|
ensureGitWorkDirEnv(installPath);
|
|
9264
9846
|
const config = loadConfigForInstallPath(installPath);
|
|
9847
|
+
const role = getServerRole(flags, config);
|
|
9265
9848
|
const withOverrides = config?.composeProfile === "full";
|
|
9266
9849
|
const installMode = getInstallMode(installPath, config);
|
|
9267
9850
|
if (flags.rebuild === true) {
|
|
9851
|
+
if (hasServiceFilter(flags)) {
|
|
9852
|
+
throw new Error("--services/--exclude cannot be combined with --rebuild. Use graceful restart for service-scoped restarts.");
|
|
9853
|
+
}
|
|
9268
9854
|
if (installMode === "image") {
|
|
9269
9855
|
throw new Error(
|
|
9270
9856
|
"Image-mode installs use prebuilt images and cannot rebuild locally. Run 'openmates server update' to pull newer images, or reinstall with --from-source to build from source."
|
|
9271
9857
|
);
|
|
9272
9858
|
}
|
|
9273
9859
|
console.error("Rebuilding OpenMates server (this may take a few minutes)...");
|
|
9274
|
-
const downArgs = [...composeArgs(installPath, withOverrides, installMode), "down"];
|
|
9860
|
+
const downArgs = [...composeArgs(installPath, withOverrides, installMode, role), "down"];
|
|
9275
9861
|
let code = await runInteractive("docker", downArgs, installPath);
|
|
9276
9862
|
if (code !== 0) process.exit(code);
|
|
9277
9863
|
try {
|
|
9278
9864
|
exec("docker volume rm openmates-cache-data", installPath);
|
|
9279
9865
|
} catch {
|
|
9280
9866
|
}
|
|
9281
|
-
const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
|
|
9867
|
+
const buildArgs = [...composeArgs(installPath, withOverrides, installMode, role), "build"];
|
|
9282
9868
|
code = await runInteractive("docker", buildArgs, installPath);
|
|
9283
9869
|
if (code !== 0) process.exit(code);
|
|
9284
|
-
const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
9870
|
+
const upArgs = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9285
9871
|
code = await runInteractive("docker", upArgs, installPath);
|
|
9286
9872
|
if (code !== 0) process.exit(code);
|
|
9287
9873
|
} else {
|
|
9288
9874
|
console.error("Restarting OpenMates server...");
|
|
9289
|
-
const args = [...composeArgs(installPath, withOverrides, installMode), "restart"];
|
|
9875
|
+
const args = [...composeArgs(installPath, withOverrides, installMode, role), "restart"];
|
|
9876
|
+
if (hasServiceFilter(flags)) args.push(...selectedComposeServices(role, flags));
|
|
9290
9877
|
const code = await runInteractive("docker", args, installPath);
|
|
9291
9878
|
if (code !== 0) process.exit(code);
|
|
9292
9879
|
}
|
|
@@ -9301,8 +9888,9 @@ async function serverLogs(flags) {
|
|
|
9301
9888
|
const installPath = resolveServerPath(flags);
|
|
9302
9889
|
ensureGitWorkDirEnv(installPath);
|
|
9303
9890
|
const config = loadConfigForInstallPath(installPath);
|
|
9891
|
+
const role = getServerRole(flags, config);
|
|
9304
9892
|
const withOverrides = config?.composeProfile === "full";
|
|
9305
|
-
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "logs"];
|
|
9893
|
+
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "logs"];
|
|
9306
9894
|
if (flags.follow === true || flags.f === true) {
|
|
9307
9895
|
args.push("--follow");
|
|
9308
9896
|
}
|
|
@@ -9312,9 +9900,13 @@ async function serverLogs(flags) {
|
|
|
9312
9900
|
} else {
|
|
9313
9901
|
args.push("--tail", "100");
|
|
9314
9902
|
}
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9903
|
+
if (hasServiceFilter(flags)) {
|
|
9904
|
+
args.push(...selectedComposeServices(role, flags));
|
|
9905
|
+
} else {
|
|
9906
|
+
const container = flags.container;
|
|
9907
|
+
if (typeof container === "string") {
|
|
9908
|
+
args.push(container);
|
|
9909
|
+
}
|
|
9318
9910
|
}
|
|
9319
9911
|
const code = await runInteractive("docker", args, installPath);
|
|
9320
9912
|
if (code !== 0) process.exit(code);
|
|
@@ -9323,7 +9915,10 @@ async function serverInstall(flags) {
|
|
|
9323
9915
|
const installPath = typeof flags.path === "string" ? resolve3(flags.path) : DEFAULT_INSTALL_PATH;
|
|
9324
9916
|
const sourcePath = typeof flags["source-path"] === "string" ? resolve3(flags["source-path"]) : null;
|
|
9325
9917
|
const fromSource = flags["from-source"] === true || sourcePath !== null;
|
|
9326
|
-
|
|
9918
|
+
const role = getServerRole(flags, null);
|
|
9919
|
+
const profile = getCoreProfile(flags, null);
|
|
9920
|
+
const runtimePlan = planServerRuntime({ role, profile, withAlerts: flags["with-alerts"] === true });
|
|
9921
|
+
if (existsSync5(join3(installPath, SOURCE_COMPOSE_FILE)) || Object.values(ROLE_IMAGE_COMPOSE_FILES).some((composeFile) => existsSync5(join3(installPath, composeFile)))) {
|
|
9327
9922
|
console.error(`OpenMates already exists at ${installPath}.`);
|
|
9328
9923
|
console.error("Use 'openmates server update' to update, or choose a different --path.");
|
|
9329
9924
|
process.exit(1);
|
|
@@ -9341,7 +9936,7 @@ async function serverInstall(flags) {
|
|
|
9341
9936
|
}
|
|
9342
9937
|
const imageTag = typeof flags["image-tag"] === "string" ? flags["image-tag"] : getDefaultImageTag();
|
|
9343
9938
|
console.error(`Preparing OpenMates image-mode install at ${installPath}...`);
|
|
9344
|
-
await writeImageModeRuntimeFiles(installPath, imageTag);
|
|
9939
|
+
await writeImageModeRuntimeFiles(installPath, imageTag, role);
|
|
9345
9940
|
const cliUrls2 = deriveSelfHostCliUrls(readFileSync5(join3(installPath, ".env"), "utf-8"));
|
|
9346
9941
|
try {
|
|
9347
9942
|
exec("docker network create openmates", installPath);
|
|
@@ -9350,7 +9945,11 @@ async function serverInstall(flags) {
|
|
|
9350
9945
|
saveServerConfig({
|
|
9351
9946
|
installPath,
|
|
9352
9947
|
installedAt: Date.now(),
|
|
9353
|
-
composeProfile: "core",
|
|
9948
|
+
composeProfile: role === "core" ? "core" : "core",
|
|
9949
|
+
serverRole: role,
|
|
9950
|
+
serverProfile: profile,
|
|
9951
|
+
defaultServices: runtimePlan.defaultServices,
|
|
9952
|
+
composeFiles: runtimePlan.composeFiles,
|
|
9354
9953
|
installMode: "image",
|
|
9355
9954
|
imageTag,
|
|
9356
9955
|
...cliUrls2
|
|
@@ -9417,6 +10016,10 @@ CLI default API: ${cliUrls2.apiUrl}`);
|
|
|
9417
10016
|
installPath,
|
|
9418
10017
|
installedAt: Date.now(),
|
|
9419
10018
|
composeProfile: "core",
|
|
10019
|
+
serverRole: role,
|
|
10020
|
+
serverProfile: profile,
|
|
10021
|
+
defaultServices: runtimePlan.defaultServices,
|
|
10022
|
+
composeFiles: runtimePlan.composeFiles,
|
|
9420
10023
|
installMode: "source",
|
|
9421
10024
|
...cliUrls
|
|
9422
10025
|
});
|
|
@@ -9434,13 +10037,71 @@ CLI default API: ${cliUrls.apiUrl}`);
|
|
|
9434
10037
|
console.log("\nOptional: edit .env first to add LLM provider API keys. Without keys, the web app and backend still start, but AI model processing is unavailable.");
|
|
9435
10038
|
}
|
|
9436
10039
|
}
|
|
9437
|
-
async function
|
|
10040
|
+
async function installContinuousUpdateService(flags) {
|
|
10041
|
+
const config = loadServerConfig();
|
|
10042
|
+
const role = getServerRole(flags, config);
|
|
10043
|
+
const channel = typeof flags.channel === "string" ? flags.channel : config?.imageChannel ?? "main";
|
|
10044
|
+
const window = typeof flags.window === "string" ? flags.window : "02:00-04:00 UTC";
|
|
10045
|
+
const plan = planContinuousUpdateService({ role, channel, window });
|
|
10046
|
+
const servicePath = join3("/etc", "systemd", "system", plan.serviceName);
|
|
10047
|
+
const timerPath = join3("/etc", "systemd", "system", plan.timerName);
|
|
10048
|
+
if (flags["dry-run"] === true || flags.json === true) {
|
|
10049
|
+
printJson({ command: "update install-service", status: "planned", role, servicePath, timerPath, unit: plan.unit, timer: plan.timer });
|
|
10050
|
+
return;
|
|
10051
|
+
}
|
|
10052
|
+
try {
|
|
10053
|
+
writeFileSync3(servicePath, plan.unit, { mode: 420 });
|
|
10054
|
+
writeFileSync3(timerPath, plan.timer, { mode: 420 });
|
|
10055
|
+
execSync("systemctl daemon-reload", { stdio: "pipe" });
|
|
10056
|
+
execSync(`systemctl enable --now ${shellQuote(plan.timerName)}`, { stdio: "pipe" });
|
|
10057
|
+
} catch (error) {
|
|
10058
|
+
throw new Error(
|
|
10059
|
+
`Could not install systemd updater. Run with sudo or use --dry-run to inspect generated units. ${error instanceof Error ? error.message : String(error)}`
|
|
10060
|
+
);
|
|
10061
|
+
}
|
|
10062
|
+
console.log(`Installed ${plan.timerName}.`);
|
|
10063
|
+
}
|
|
10064
|
+
async function serverUpdate(rest, flags) {
|
|
10065
|
+
if (rest[0] === "status") {
|
|
10066
|
+
const installPath2 = resolveServerPath(flags);
|
|
10067
|
+
const config2 = loadConfigForInstallPath(installPath2);
|
|
10068
|
+
const role2 = getServerRole(flags, config2);
|
|
10069
|
+
const filePath = updateStatusFile(installPath2, role2);
|
|
10070
|
+
if (flags.json === true) {
|
|
10071
|
+
printJson(existsSync5(filePath) ? JSON.parse(readFileSync5(filePath, "utf-8")) : { role: role2, status: "unknown" });
|
|
10072
|
+
return;
|
|
10073
|
+
}
|
|
10074
|
+
if (!existsSync5(filePath)) {
|
|
10075
|
+
console.log(`No update status recorded for ${role2}.`);
|
|
10076
|
+
return;
|
|
10077
|
+
}
|
|
10078
|
+
console.log(readFileSync5(filePath, "utf-8").trim());
|
|
10079
|
+
return;
|
|
10080
|
+
}
|
|
10081
|
+
if (rest[0] === "install-service") {
|
|
10082
|
+
if (flags.continuous !== true) throw new Error("Usage: openmates server update install-service --continuous [--channel main|dev|stable]");
|
|
10083
|
+
await installContinuousUpdateService(flags);
|
|
10084
|
+
return;
|
|
10085
|
+
}
|
|
10086
|
+
if (flags.continuous === true) {
|
|
10087
|
+
const intervalMinutes = typeof flags.interval === "string" ? Number.parseInt(flags.interval, 10) : 30;
|
|
10088
|
+
if (!Number.isFinite(intervalMinutes) || intervalMinutes < 5) throw new Error("--interval must be at least 5 minutes.");
|
|
10089
|
+
console.error(`Running continuous updater every ${intervalMinutes} minutes. Use Ctrl+C to stop.`);
|
|
10090
|
+
while (true) {
|
|
10091
|
+
await serverUpdate([], { ...flags, continuous: false });
|
|
10092
|
+
await sleep2(intervalMinutes * 6e4);
|
|
10093
|
+
}
|
|
10094
|
+
}
|
|
9438
10095
|
const installPath = resolveServerPath(flags);
|
|
9439
10096
|
const dryRun = flags["dry-run"] === true;
|
|
9440
10097
|
if (!dryRun) ensureGitWorkDirEnv(installPath);
|
|
9441
10098
|
const config = loadConfigForInstallPath(installPath);
|
|
10099
|
+
const role = getServerRole(flags, config);
|
|
9442
10100
|
const withOverrides = config?.composeProfile === "full";
|
|
9443
10101
|
const installMode = getInstallMode(installPath, config);
|
|
10102
|
+
const filterRequested = hasServiceFilter(flags);
|
|
10103
|
+
const selectedServices = filterRequested ? selectedComposeServices(role, flags) : [];
|
|
10104
|
+
const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
|
|
9444
10105
|
if (installMode === "source" && (flags["image-tag"] !== void 0 || flags.channel !== void 0)) {
|
|
9445
10106
|
throw new Error("--image-tag and --channel only apply to image-mode installs. Source-mode installs update from Git.");
|
|
9446
10107
|
}
|
|
@@ -9450,14 +10111,22 @@ async function serverUpdate(flags) {
|
|
|
9450
10111
|
const currentTag = getImageTagFromEnv(installPath, config);
|
|
9451
10112
|
const target = resolveTargetImageTag(flags, currentTag, getPackageVersion());
|
|
9452
10113
|
const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
|
|
10114
|
+
const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingEnvKeys });
|
|
9453
10115
|
const plan = {
|
|
9454
10116
|
command: "update",
|
|
10117
|
+
role,
|
|
9455
10118
|
path: installPath,
|
|
9456
10119
|
mode: "image",
|
|
9457
10120
|
currentImageTag: currentTag || null,
|
|
9458
10121
|
targetImageTag: target.tag,
|
|
9459
10122
|
channel: target.channel ?? null,
|
|
9460
10123
|
templateRef,
|
|
10124
|
+
selectedServices: filterRequested ? selectedServices : "all",
|
|
10125
|
+
steps: safetyPlan.steps,
|
|
10126
|
+
backupName: safetyPlan.backupName,
|
|
10127
|
+
missingRequiredEnvKeys: missingEnvKeys,
|
|
10128
|
+
blocked: safetyPlan.blocked,
|
|
10129
|
+
blockReason: safetyPlan.blockReason,
|
|
9461
10130
|
dryRun
|
|
9462
10131
|
};
|
|
9463
10132
|
if (dryRun) {
|
|
@@ -9469,37 +10138,59 @@ async function serverUpdate(flags) {
|
|
|
9469
10138
|
console.log(` Current tag: ${currentTag || "unknown"}`);
|
|
9470
10139
|
console.log(` Target tag: ${target.tag}`);
|
|
9471
10140
|
console.log(` Template ref: ${templateRef}`);
|
|
10141
|
+
console.log(` Role: ${role}`);
|
|
10142
|
+
console.log(` Services: ${filterRequested ? selectedServices.join(", ") : "all"}`);
|
|
10143
|
+
console.log(` Backup: ${safetyPlan.backupName ?? "none"}`);
|
|
10144
|
+
console.log(` Steps: ${safetyPlan.steps.join(" -> ")}`);
|
|
10145
|
+
console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
|
|
9472
10146
|
console.log(" Commands: refresh compose, docker compose pull, docker compose up -d, health checks");
|
|
9473
10147
|
}
|
|
9474
10148
|
return;
|
|
9475
10149
|
}
|
|
10150
|
+
if (safetyPlan.blocked) throw new Error(safetyPlan.blockReason ?? "Update blocked by preflight.");
|
|
10151
|
+
if (missingEnvKeys.length && flags.yes !== true) {
|
|
10152
|
+
throw new Error(`Required environment keys are missing: ${missingEnvKeys.join(", ")}. Add them to .env or rerun with --yes after reviewing.`);
|
|
10153
|
+
}
|
|
9476
10154
|
console.error(`Mode: image`);
|
|
9477
10155
|
console.error(`Current image tag: ${currentTag || "unknown"}`);
|
|
9478
10156
|
console.error(`Target image tag: ${target.tag}`);
|
|
10157
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "backup" });
|
|
10158
|
+
if (safetyPlan.backupName) {
|
|
10159
|
+
console.error(`Creating rotating pre-update backup: ${safetyPlan.backupName}`);
|
|
10160
|
+
createServerBackup(installPath, role, { preUpdate: true });
|
|
10161
|
+
} else {
|
|
10162
|
+
console.error("Skipping pre-update backup for this role or because --skip-backup was passed.");
|
|
10163
|
+
}
|
|
9479
10164
|
console.error(`Refreshing self-host runtime files from ${templateRef}...`);
|
|
9480
|
-
await writeImageModeRuntimeFiles(installPath, target.tag);
|
|
9481
|
-
const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
|
|
10165
|
+
await writeImageModeRuntimeFiles(installPath, target.tag, role);
|
|
10166
|
+
const pullArgs = [...composeArgs(installPath, withOverrides, installMode, role), "pull"];
|
|
10167
|
+
if (filterRequested) pullArgs.push(...selectedServices);
|
|
9482
10168
|
let code2 = 0;
|
|
9483
10169
|
if (shouldPullImages()) {
|
|
10170
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "pull" });
|
|
9484
10171
|
console.error("Pulling prebuilt images...");
|
|
9485
10172
|
code2 = await runInteractive("docker", pullArgs, installPath);
|
|
9486
10173
|
if (code2 !== 0) process.exit(code2);
|
|
9487
10174
|
} else {
|
|
9488
10175
|
console.error("Skipping image pull because OPENMATES_SKIP_IMAGE_PULL=1.");
|
|
9489
10176
|
}
|
|
9490
|
-
|
|
10177
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "up" });
|
|
10178
|
+
const upArgs2 = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
10179
|
+
if (filterRequested) upArgs2.push(...selectedServices);
|
|
9491
10180
|
code2 = await runInteractive("docker", upArgs2, installPath);
|
|
9492
10181
|
if (code2 !== 0) process.exit(code2);
|
|
9493
|
-
console.error("Waiting for
|
|
10182
|
+
console.error("Waiting for role health checks...");
|
|
9494
10183
|
try {
|
|
9495
|
-
|
|
10184
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "health-check" });
|
|
10185
|
+
await waitForServerHealth(installPath, role);
|
|
9496
10186
|
} catch (error) {
|
|
9497
|
-
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode), "ps"], installPath);
|
|
10187
|
+
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "ps"], installPath);
|
|
9498
10188
|
throw error;
|
|
9499
10189
|
}
|
|
9500
10190
|
if (config) {
|
|
9501
10191
|
saveServerConfig({ ...config, imageTag: target.tag, imageChannel: target.channel });
|
|
9502
10192
|
}
|
|
10193
|
+
writeUpdateStatus(installPath, role, { status: "success", targetImageTag: target.tag, step: "complete" });
|
|
9503
10194
|
if (flags.json === true) {
|
|
9504
10195
|
printJson({ ...plan, status: "success", dryRun: false });
|
|
9505
10196
|
} else {
|
|
@@ -9545,18 +10236,18 @@ async function serverUpdate(flags) {
|
|
|
9545
10236
|
} catch {
|
|
9546
10237
|
}
|
|
9547
10238
|
}
|
|
9548
|
-
const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
|
|
10239
|
+
const buildArgs = [...composeArgs(installPath, withOverrides, installMode, role), "build"];
|
|
9549
10240
|
console.error("Rebuilding containers...");
|
|
9550
10241
|
let code = await runInteractive("docker", buildArgs, installPath);
|
|
9551
10242
|
if (code !== 0) process.exit(code);
|
|
9552
|
-
const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
10243
|
+
const upArgs = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9553
10244
|
code = await runInteractive("docker", upArgs, installPath);
|
|
9554
10245
|
if (code !== 0) process.exit(code);
|
|
9555
10246
|
console.error("Waiting for API and web health checks...");
|
|
9556
10247
|
try {
|
|
9557
|
-
await waitForServerHealth(installPath);
|
|
10248
|
+
await waitForServerHealth(installPath, role);
|
|
9558
10249
|
} catch (error) {
|
|
9559
|
-
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode), "ps"], installPath);
|
|
10250
|
+
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "ps"], installPath);
|
|
9560
10251
|
throw error;
|
|
9561
10252
|
}
|
|
9562
10253
|
if (flags.json === true) {
|
|
@@ -9570,6 +10261,7 @@ async function serverReset(flags) {
|
|
|
9570
10261
|
const installPath = resolveServerPath(flags);
|
|
9571
10262
|
ensureGitWorkDirEnv(installPath);
|
|
9572
10263
|
const config = loadConfigForInstallPath(installPath);
|
|
10264
|
+
const role = getServerRole(flags, config);
|
|
9573
10265
|
const withOverrides = config?.composeProfile === "full";
|
|
9574
10266
|
const installMode = getInstallMode(installPath, config);
|
|
9575
10267
|
const userDataOnly = flags["delete-user-data-only"] === true;
|
|
@@ -9591,7 +10283,7 @@ async function serverReset(flags) {
|
|
|
9591
10283
|
}
|
|
9592
10284
|
console.error("Resetting server...");
|
|
9593
10285
|
if (userDataOnly) {
|
|
9594
|
-
const downArgs = [...composeArgs(installPath, withOverrides, installMode), "down"];
|
|
10286
|
+
const downArgs = [...composeArgs(installPath, withOverrides, installMode, role), "down"];
|
|
9595
10287
|
let code = await runInteractive("docker", downArgs, installPath);
|
|
9596
10288
|
if (code !== 0) process.exit(code);
|
|
9597
10289
|
for (const vol of ["openmates-cache-data", "openmates-postgres-data", "openmates-cms-database-data"]) {
|
|
@@ -9602,15 +10294,15 @@ async function serverReset(flags) {
|
|
|
9602
10294
|
}
|
|
9603
10295
|
}
|
|
9604
10296
|
if (installMode === "source") {
|
|
9605
|
-
const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
|
|
10297
|
+
const buildArgs = [...composeArgs(installPath, withOverrides, installMode, role), "build"];
|
|
9606
10298
|
code = await runInteractive("docker", buildArgs, installPath);
|
|
9607
10299
|
if (code !== 0) process.exit(code);
|
|
9608
10300
|
}
|
|
9609
|
-
const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
10301
|
+
const upArgs = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9610
10302
|
code = await runInteractive("docker", upArgs, installPath);
|
|
9611
10303
|
if (code !== 0) process.exit(code);
|
|
9612
10304
|
} else {
|
|
9613
|
-
const args = [...composeArgs(installPath, withOverrides, installMode), "down", "-v"];
|
|
10305
|
+
const args = [...composeArgs(installPath, withOverrides, installMode, role), "down", "-v"];
|
|
9614
10306
|
const code = await runInteractive("docker", args, installPath);
|
|
9615
10307
|
if (code !== 0) process.exit(code);
|
|
9616
10308
|
}
|
|
@@ -9888,11 +10580,75 @@ async function serverAi(rest, flags) {
|
|
|
9888
10580
|
throw new Error(`Unknown server ai models command '${action}'. Use add, list, test, or remove.`);
|
|
9889
10581
|
}
|
|
9890
10582
|
}
|
|
10583
|
+
async function serverBackup(rest, flags) {
|
|
10584
|
+
const action = rest[0];
|
|
10585
|
+
const installPath = resolveServerPath(flags);
|
|
10586
|
+
const config = loadConfigForInstallPath(installPath);
|
|
10587
|
+
const role = getServerRole(flags, config);
|
|
10588
|
+
if (action === "list") {
|
|
10589
|
+
const dir = roleBackupDir(installPath, role);
|
|
10590
|
+
const files = existsSync5(dir) ? readdirSync(dir).filter((item) => item.endsWith(".tar.gz")).sort() : [];
|
|
10591
|
+
if (flags.json === true) {
|
|
10592
|
+
printJson({ role, backupDir: dir, files });
|
|
10593
|
+
return;
|
|
10594
|
+
}
|
|
10595
|
+
console.log(`Backups for ${role}:`);
|
|
10596
|
+
if (!files.length) {
|
|
10597
|
+
console.log(" none");
|
|
10598
|
+
return;
|
|
10599
|
+
}
|
|
10600
|
+
for (const file of files) console.log(` ${join3(dir, file)}`);
|
|
10601
|
+
return;
|
|
10602
|
+
}
|
|
10603
|
+
requireDocker();
|
|
10604
|
+
const output = typeof flags.output === "string" ? flags.output : void 0;
|
|
10605
|
+
const archivePath = createServerBackup(installPath, role, {
|
|
10606
|
+
output,
|
|
10607
|
+
includeObservability: flags["include-observability"] === true
|
|
10608
|
+
});
|
|
10609
|
+
if (flags.json === true) {
|
|
10610
|
+
printJson({ command: "backup", status: "success", role, file: archivePath });
|
|
10611
|
+
} else {
|
|
10612
|
+
console.log(`Backup created: ${archivePath}`);
|
|
10613
|
+
}
|
|
10614
|
+
}
|
|
10615
|
+
async function serverRestore(flags) {
|
|
10616
|
+
requireDocker();
|
|
10617
|
+
const installPath = resolveServerPath(flags);
|
|
10618
|
+
const config = loadConfigForInstallPath(installPath);
|
|
10619
|
+
const role = getServerRole(flags, config);
|
|
10620
|
+
const file = typeof flags.file === "string" ? flags.file : "";
|
|
10621
|
+
if (!file) throw new Error("Usage: openmates server restore --file <backup.tar.gz> [--role core|upload|preview] [--yes]");
|
|
10622
|
+
const restorePlan = planRestore({ role, file, yes: flags.yes === true });
|
|
10623
|
+
if (restorePlan.requiresConfirmation) {
|
|
10624
|
+
console.error(`
|
|
10625
|
+
WARNING: This will restore ${role} data from ${file}.`);
|
|
10626
|
+
const confirmed = await confirmDestructive("RESTORE OPENMATES BACKUP");
|
|
10627
|
+
if (!confirmed) {
|
|
10628
|
+
console.error("Restore cancelled.");
|
|
10629
|
+
return;
|
|
10630
|
+
}
|
|
10631
|
+
}
|
|
10632
|
+
const withOverrides = config?.composeProfile === "full";
|
|
10633
|
+
const installMode = getInstallMode(installPath, config);
|
|
10634
|
+
let code = await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "stop"], installPath);
|
|
10635
|
+
if (code !== 0) process.exit(code);
|
|
10636
|
+
restoreServerBackup(installPath, role, file);
|
|
10637
|
+
code = await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"], installPath);
|
|
10638
|
+
if (code !== 0) process.exit(code);
|
|
10639
|
+
await waitForServerHealth(installPath, role);
|
|
10640
|
+
if (flags.json === true) {
|
|
10641
|
+
printJson({ command: "restore", status: "success", role, file });
|
|
10642
|
+
} else {
|
|
10643
|
+
console.log(`Restore completed for ${role}.`);
|
|
10644
|
+
}
|
|
10645
|
+
}
|
|
9891
10646
|
async function serverUninstall(flags) {
|
|
9892
10647
|
requireDocker();
|
|
9893
10648
|
const installPath = resolveServerPath(flags);
|
|
9894
10649
|
ensureGitWorkDirEnv(installPath);
|
|
9895
10650
|
const config = loadConfigForInstallPath(installPath);
|
|
10651
|
+
const role = getServerRole(flags, config);
|
|
9896
10652
|
const withOverrides = config?.composeProfile === "full";
|
|
9897
10653
|
const keepData = flags["keep-data"] === true;
|
|
9898
10654
|
console.error("\nWARNING: This will completely uninstall OpenMates:");
|
|
@@ -9914,7 +10670,7 @@ async function serverUninstall(flags) {
|
|
|
9914
10670
|
}
|
|
9915
10671
|
}
|
|
9916
10672
|
console.error("Uninstalling OpenMates...");
|
|
9917
|
-
const downArgs = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "down", "--rmi", "local"];
|
|
10673
|
+
const downArgs = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "down", "--rmi", "local"];
|
|
9918
10674
|
if (!keepData) {
|
|
9919
10675
|
downArgs.push("-v");
|
|
9920
10676
|
}
|
|
@@ -9940,6 +10696,113 @@ async function serverUninstall(flags) {
|
|
|
9940
10696
|
}
|
|
9941
10697
|
}
|
|
9942
10698
|
}
|
|
10699
|
+
async function serverPreflight(flags) {
|
|
10700
|
+
const installPath = resolveServerPath(flags);
|
|
10701
|
+
const config = loadConfigForInstallPath(installPath);
|
|
10702
|
+
const role = getServerRole(flags, config);
|
|
10703
|
+
const services = hasServiceFilter(flags) ? selectedComposeServices(role, flags) : [];
|
|
10704
|
+
const updatePlan = planUpdate({ role, selectedServices: services, dryRun: true });
|
|
10705
|
+
const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
|
|
10706
|
+
const runtimePlan = planServerRuntime({
|
|
10707
|
+
role,
|
|
10708
|
+
profile: getCoreProfile(flags, config),
|
|
10709
|
+
withAlerts: flags["with-alerts"] === true
|
|
10710
|
+
});
|
|
10711
|
+
const caddyPlan = planCaddyCommand({ role, action: "status" });
|
|
10712
|
+
const preflight = {
|
|
10713
|
+
command: "preflight",
|
|
10714
|
+
status: updatePlan.blocked ? "blocked" : "ok",
|
|
10715
|
+
path: installPath,
|
|
10716
|
+
role,
|
|
10717
|
+
profile: runtimePlan.profile,
|
|
10718
|
+
services: hasServiceFilter(flags) ? services : "all",
|
|
10719
|
+
healthChecks: runtimePlan.healthChecks,
|
|
10720
|
+
updateSteps: updatePlan.steps,
|
|
10721
|
+
backupName: updatePlan.backupName,
|
|
10722
|
+
missingRequiredEnvKeys: missingEnvKeys,
|
|
10723
|
+
caddy: caddyPlan
|
|
10724
|
+
};
|
|
10725
|
+
if (flags.json === true) {
|
|
10726
|
+
printJson(preflight);
|
|
10727
|
+
return;
|
|
10728
|
+
}
|
|
10729
|
+
console.log("Server preflight plan:");
|
|
10730
|
+
console.log(` Role: ${role}`);
|
|
10731
|
+
console.log(` Profile: ${runtimePlan.profile ?? "n/a"}`);
|
|
10732
|
+
console.log(` Services: ${hasServiceFilter(flags) ? services.join(", ") : "all"}`);
|
|
10733
|
+
console.log(` Backup: ${updatePlan.backupName ?? "none"}`);
|
|
10734
|
+
console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
|
|
10735
|
+
console.log(` Health checks: ${runtimePlan.healthChecks.join(", ")}`);
|
|
10736
|
+
console.log(` Caddy steps: ${caddyPlan.steps.join(" -> ")}`);
|
|
10737
|
+
}
|
|
10738
|
+
async function serverCaddy(rest, flags) {
|
|
10739
|
+
const action = rest[0] ?? "status";
|
|
10740
|
+
if (!["check", "status", "diff", "apply"].includes(action)) {
|
|
10741
|
+
throw new Error("Usage: openmates server caddy check|status|diff|apply [--role core|upload|preview]");
|
|
10742
|
+
}
|
|
10743
|
+
const config = loadServerConfig();
|
|
10744
|
+
const role = getServerRole(flags, config);
|
|
10745
|
+
const appliedPath = typeof flags.config === "string" ? flags.config : "/etc/caddy/Caddyfile";
|
|
10746
|
+
const templatePath = packagedCaddyTemplatePath(role);
|
|
10747
|
+
if (!existsSync5(templatePath)) throw new Error(`Packaged Caddy template not found: ${templatePath}`);
|
|
10748
|
+
const plan = planCaddyCommand({ role, action, appliedPath });
|
|
10749
|
+
const payload = {
|
|
10750
|
+
...plan,
|
|
10751
|
+
templatePath,
|
|
10752
|
+
templateHash: fileHash(templatePath),
|
|
10753
|
+
appliedHash: fileHash(appliedPath),
|
|
10754
|
+
drift: fileHash(templatePath) !== fileHash(appliedPath)
|
|
10755
|
+
};
|
|
10756
|
+
if (flags.json === true) {
|
|
10757
|
+
printJson(payload);
|
|
10758
|
+
return;
|
|
10759
|
+
}
|
|
10760
|
+
if (action === "check") {
|
|
10761
|
+
execSync(`caddy validate --config ${shellQuote(templatePath)}`, { stdio: "inherit" });
|
|
10762
|
+
console.log(`Caddy template is valid for ${role}.`);
|
|
10763
|
+
return;
|
|
10764
|
+
}
|
|
10765
|
+
if (action === "diff") {
|
|
10766
|
+
if (!existsSync5(appliedPath)) throw new Error(`Applied Caddyfile not found: ${appliedPath}`);
|
|
10767
|
+
try {
|
|
10768
|
+
execSync(`diff -u ${shellQuote(appliedPath)} ${shellQuote(templatePath)}`, { stdio: "inherit" });
|
|
10769
|
+
} catch (error) {
|
|
10770
|
+
const status = typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
10771
|
+
if (status !== 1) throw error;
|
|
10772
|
+
}
|
|
10773
|
+
return;
|
|
10774
|
+
}
|
|
10775
|
+
if (action === "apply") {
|
|
10776
|
+
execSync(`caddy validate --config ${shellQuote(templatePath)}`, { stdio: "inherit" });
|
|
10777
|
+
if (flags.yes !== true) {
|
|
10778
|
+
console.error(`
|
|
10779
|
+
WARNING: This will replace ${appliedPath} with the packaged ${role} Caddyfile.`);
|
|
10780
|
+
const confirmed = await confirmDestructive("APPLY CADDYFILE");
|
|
10781
|
+
if (!confirmed) {
|
|
10782
|
+
console.error("Caddy apply cancelled.");
|
|
10783
|
+
return;
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10786
|
+
const backupPath = `${appliedPath}.openmates-backup-${nowStamp()}`;
|
|
10787
|
+
try {
|
|
10788
|
+
if (existsSync5(appliedPath)) copyFileSync(appliedPath, backupPath);
|
|
10789
|
+
copyFileSync(templatePath, appliedPath);
|
|
10790
|
+
execSync("systemctl reload caddy", { stdio: "inherit" });
|
|
10791
|
+
} catch (error) {
|
|
10792
|
+
throw new Error(`Could not apply Caddyfile. Run with sudo or use --config <writable path>. ${error instanceof Error ? error.message : String(error)}`);
|
|
10793
|
+
}
|
|
10794
|
+
console.log(`Applied Caddyfile for ${role}. Backup: ${backupPath}`);
|
|
10795
|
+
return;
|
|
10796
|
+
}
|
|
10797
|
+
console.log(`Caddy ${action} plan:`);
|
|
10798
|
+
console.log(` Role: ${payload.role}`);
|
|
10799
|
+
console.log(` Template: ${payload.templatePath}`);
|
|
10800
|
+
console.log(` Applied: ${payload.appliedPath}`);
|
|
10801
|
+
console.log(` Template hash: ${payload.templateHash ?? "missing"}`);
|
|
10802
|
+
console.log(` Applied hash: ${payload.appliedHash ?? "missing"}`);
|
|
10803
|
+
console.log(` Drift: ${payload.drift ? "yes" : "no"}`);
|
|
10804
|
+
console.log(` Steps: ${payload.steps.join(" -> ")}`);
|
|
10805
|
+
}
|
|
9943
10806
|
function printServerHelp() {
|
|
9944
10807
|
console.log(`
|
|
9945
10808
|
OpenMates Server Management
|
|
@@ -9954,6 +10817,10 @@ Commands:
|
|
|
9954
10817
|
status Show server status (container health)
|
|
9955
10818
|
logs Display server logs
|
|
9956
10819
|
update Update to latest version (pull images, or git pull + rebuild for source installs)
|
|
10820
|
+
preflight Show role/update/Caddy preflight plan
|
|
10821
|
+
caddy Plan host-level Caddyfile check/status/diff/apply operations
|
|
10822
|
+
backup Create or list backups
|
|
10823
|
+
restore Restore a backup archive
|
|
9957
10824
|
make-admin Grant admin privileges to a user
|
|
9958
10825
|
ai Manage self-hosted local AI models
|
|
9959
10826
|
reset Reset server data (requires confirmation)
|
|
@@ -9962,33 +10829,57 @@ Commands:
|
|
|
9962
10829
|
Global Options:
|
|
9963
10830
|
--path <dir> Override the server installation directory
|
|
9964
10831
|
--json Output machine-readable JSON
|
|
10832
|
+
--role <role> Server role: core, upload, or preview (default: core)
|
|
9965
10833
|
--help Show this help message
|
|
9966
10834
|
|
|
9967
10835
|
Command Options:
|
|
9968
10836
|
install:
|
|
9969
10837
|
--path <dir> Install directory (default: ~/openmates)
|
|
9970
10838
|
--env-path <file> Copy a pre-existing .env file during install
|
|
10839
|
+
--profile <name> Core profile: minimal, standard, or production
|
|
10840
|
+
--with-alerts Include alertmanager in production profile planning
|
|
9971
10841
|
--image-tag <tag> Prebuilt image tag (default: CLI version tag)
|
|
9972
10842
|
--from-source Clone/build from source instead of using prebuilt GHCR images
|
|
9973
10843
|
--source-path <dir> Clone from a local checkout instead of GitHub (implies --from-source)
|
|
9974
10844
|
|
|
9975
10845
|
start:
|
|
9976
10846
|
--with-overrides Include admin UIs (Directus CMS, Grafana)
|
|
10847
|
+
--services <csv> Start only selected role services
|
|
10848
|
+
--exclude <csv> Start all role services except selected services
|
|
9977
10849
|
|
|
9978
10850
|
restart:
|
|
9979
10851
|
--rebuild Full rebuild (down + build + up) instead of graceful restart
|
|
10852
|
+
--services <csv> Restart only selected role services
|
|
10853
|
+
--exclude <csv> Restart all role services except selected services
|
|
9980
10854
|
|
|
9981
10855
|
logs:
|
|
9982
10856
|
--container <name> Filter logs to a specific service (e.g. api, cms)
|
|
10857
|
+
--services <csv> Filter logs to selected role services
|
|
10858
|
+
--exclude <csv> Filter logs to all role services except selected services
|
|
9983
10859
|
--follow, -f Stream logs in real time
|
|
9984
10860
|
--tail <n> Number of lines to show (default: 100)
|
|
9985
10861
|
|
|
9986
10862
|
update:
|
|
9987
10863
|
--dry-run Show update plan without changing files or containers
|
|
10864
|
+
--services <csv> Update only selected role services
|
|
10865
|
+
--exclude <csv> Update all role services except selected services
|
|
9988
10866
|
--image-tag <tag> Image mode: update to a specific prebuilt image tag
|
|
9989
10867
|
--channel <name> Image mode: update using stable/main or dev channel tags
|
|
10868
|
+
--continuous Run continuously in foreground, or use with install-service
|
|
10869
|
+
--interval <min> Foreground continuous update interval (default: 30)
|
|
10870
|
+
install-service --continuous --channel <name> --window <window>
|
|
9990
10871
|
--force Source mode: stash local changes before pulling
|
|
9991
10872
|
|
|
10873
|
+
backup:
|
|
10874
|
+
openmates server backup [--role core|upload|preview] [--output <file>] [--include-observability]
|
|
10875
|
+
openmates server backup list [--role core|upload|preview]
|
|
10876
|
+
|
|
10877
|
+
restore:
|
|
10878
|
+
openmates server restore --file <backup.tar.gz> [--role core|upload|preview] [--yes]
|
|
10879
|
+
|
|
10880
|
+
caddy:
|
|
10881
|
+
openmates server caddy check|status|diff|apply [--role core|upload|preview] [--config /etc/caddy/Caddyfile]
|
|
10882
|
+
|
|
9992
10883
|
reset:
|
|
9993
10884
|
--delete-user-data-only Only delete database and cache (preserve config)
|
|
9994
10885
|
--yes Skip confirmation prompt
|
|
@@ -10047,7 +10938,15 @@ async function handleServer(subcommand, rest, flags) {
|
|
|
10047
10938
|
case "install":
|
|
10048
10939
|
return serverInstall(flags);
|
|
10049
10940
|
case "update":
|
|
10050
|
-
return serverUpdate(flags);
|
|
10941
|
+
return serverUpdate(rest, flags);
|
|
10942
|
+
case "preflight":
|
|
10943
|
+
return serverPreflight(flags);
|
|
10944
|
+
case "caddy":
|
|
10945
|
+
return serverCaddy(rest, flags);
|
|
10946
|
+
case "backup":
|
|
10947
|
+
return serverBackup(rest, flags);
|
|
10948
|
+
case "restore":
|
|
10949
|
+
return serverRestore(flags);
|
|
10051
10950
|
case "reset":
|
|
10052
10951
|
return serverReset(flags);
|
|
10053
10952
|
case "make-admin":
|
|
@@ -26725,6 +27624,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
26725
27624
|
text: "Upload PDFs and read or search their content."
|
|
26726
27625
|
}
|
|
26727
27626
|
},
|
|
27627
|
+
mindmaps: {
|
|
27628
|
+
text: "Mind Maps",
|
|
27629
|
+
description: {
|
|
27630
|
+
text: "Create structured idea maps and brainstorm topic trees."
|
|
27631
|
+
}
|
|
27632
|
+
},
|
|
26728
27633
|
math: {
|
|
26729
27634
|
text: "Math",
|
|
26730
27635
|
description: {
|
|
@@ -26788,6 +27693,47 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
26788
27693
|
generated_by_cost: {
|
|
26789
27694
|
text: "Cost: {credits} credits"
|
|
26790
27695
|
},
|
|
27696
|
+
interests: {
|
|
27697
|
+
eyebrow: {
|
|
27698
|
+
text: "Private local personalization"
|
|
27699
|
+
},
|
|
27700
|
+
title: {
|
|
27701
|
+
text: "Choose what you care about. Your picks stay in this browser session."
|
|
27702
|
+
},
|
|
27703
|
+
continue: {
|
|
27704
|
+
text: "Continue"
|
|
27705
|
+
},
|
|
27706
|
+
software_development: {
|
|
27707
|
+
text: "software development"
|
|
27708
|
+
},
|
|
27709
|
+
use_the_cli: {
|
|
27710
|
+
text: "use the CLI"
|
|
27711
|
+
},
|
|
27712
|
+
open_source: {
|
|
27713
|
+
text: "open source"
|
|
27714
|
+
},
|
|
27715
|
+
read_developer_docs: {
|
|
27716
|
+
text: "read developer docs"
|
|
27717
|
+
},
|
|
27718
|
+
run_code: {
|
|
27719
|
+
text: "run code"
|
|
27720
|
+
},
|
|
27721
|
+
protect_my_privacy: {
|
|
27722
|
+
text: "protect my privacy"
|
|
27723
|
+
},
|
|
27724
|
+
summarize_documents: {
|
|
27725
|
+
text: "summarize documents"
|
|
27726
|
+
},
|
|
27727
|
+
find_apartments: {
|
|
27728
|
+
text: "find apartments"
|
|
27729
|
+
},
|
|
27730
|
+
local_life: {
|
|
27731
|
+
text: "local life"
|
|
27732
|
+
},
|
|
27733
|
+
learn_anything: {
|
|
27734
|
+
text: "learn anything"
|
|
27735
|
+
}
|
|
27736
|
+
},
|
|
26791
27737
|
new_chat: {
|
|
26792
27738
|
text: "New chat"
|
|
26793
27739
|
},
|
|
@@ -27213,6 +28159,9 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
27213
28159
|
learn_coding: {
|
|
27214
28160
|
text: "How do I start learning to code?"
|
|
27215
28161
|
},
|
|
28162
|
+
use_openmates_cli_api: {
|
|
28163
|
+
text: "Show me how to use OpenMates from the CLI or API"
|
|
28164
|
+
},
|
|
27216
28165
|
stock_market: {
|
|
27217
28166
|
text: "Explain how the stock market works"
|
|
27218
28167
|
},
|
|
@@ -30378,6 +31327,32 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
30378
31327
|
text: "Plot"
|
|
30379
31328
|
}
|
|
30380
31329
|
},
|
|
31330
|
+
mindmaps: {
|
|
31331
|
+
mindmap: {
|
|
31332
|
+
text: "Mind Map"
|
|
31333
|
+
},
|
|
31334
|
+
counts: {
|
|
31335
|
+
text: "{nodes} nodes \xB7 {edges} edges"
|
|
31336
|
+
},
|
|
31337
|
+
invalid_json: {
|
|
31338
|
+
text: "Invalid mind map JSON"
|
|
31339
|
+
},
|
|
31340
|
+
invalid_content: {
|
|
31341
|
+
text: "Invalid content"
|
|
31342
|
+
},
|
|
31343
|
+
validation_warnings: {
|
|
31344
|
+
text: "Validation warnings"
|
|
31345
|
+
},
|
|
31346
|
+
source: {
|
|
31347
|
+
text: "Source"
|
|
31348
|
+
},
|
|
31349
|
+
expand: {
|
|
31350
|
+
text: "Expand {label}"
|
|
31351
|
+
},
|
|
31352
|
+
collapse: {
|
|
31353
|
+
text: "Collapse {label}"
|
|
31354
|
+
}
|
|
31355
|
+
},
|
|
30381
31356
|
diagrams: {
|
|
30382
31357
|
mermaid: {
|
|
30383
31358
|
text: "Mermaid Diagram"
|
|
@@ -36409,6 +37384,21 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
|
|
|
36409
37384
|
},
|
|
36410
37385
|
import_importing: {
|
|
36411
37386
|
text: "Importing\u2026"
|
|
37387
|
+
},
|
|
37388
|
+
interests: {
|
|
37389
|
+
text: "Interests"
|
|
37390
|
+
},
|
|
37391
|
+
interests_description: {
|
|
37392
|
+
text: "Choose the topics OpenMates should prioritize when it suggests chats, examples, and product tips."
|
|
37393
|
+
},
|
|
37394
|
+
interests_privacy_note: {
|
|
37395
|
+
text: "These preferences are encrypted on your device before syncing. The server stores only ciphertext."
|
|
37396
|
+
},
|
|
37397
|
+
interests_saved: {
|
|
37398
|
+
text: "Interests saved"
|
|
37399
|
+
},
|
|
37400
|
+
interests_save_error: {
|
|
37401
|
+
text: "Could not save interests. Please try again."
|
|
36412
37402
|
}
|
|
36413
37403
|
},
|
|
36414
37404
|
ai: {
|
|
@@ -39929,6 +40919,9 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
|
|
|
39929
40919
|
server_will_be_restarted: {
|
|
39930
40920
|
text: "The server will be restarted and\ntherefore briefly offline once\nthe update has been installed."
|
|
39931
40921
|
},
|
|
40922
|
+
cli_managed_update_notice: {
|
|
40923
|
+
text: "Install updates from the server host with <code>openmates server update</code>."
|
|
40924
|
+
},
|
|
39932
40925
|
installing_update: {
|
|
39933
40926
|
text: "Installing update..."
|
|
39934
40927
|
},
|
|
@@ -42301,7 +43294,7 @@ function buildAssistantFeedbackDecision(rating) {
|
|
|
42301
43294
|
|
|
42302
43295
|
// src/benchmark.ts
|
|
42303
43296
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
42304
|
-
import { existsSync as existsSync6, mkdtempSync, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync4 } from "fs";
|
|
43297
|
+
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
42305
43298
|
import { tmpdir } from "os";
|
|
42306
43299
|
import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
|
|
42307
43300
|
import { fileURLToPath } from "url";
|
|
@@ -43100,7 +44093,7 @@ function loadProviderPricing() {
|
|
|
43100
44093
|
const providersDir = findProvidersDir();
|
|
43101
44094
|
const pricing = /* @__PURE__ */ new Map();
|
|
43102
44095
|
if (!providersDir) return pricing;
|
|
43103
|
-
for (const fileName of
|
|
44096
|
+
for (const fileName of readdirSync2(providersDir)) {
|
|
43104
44097
|
if (!fileName.endsWith(".yml")) continue;
|
|
43105
44098
|
const filePath = join4(providersDir, fileName);
|
|
43106
44099
|
const text = readFileSync6(filePath, "utf-8");
|
|
@@ -43285,7 +44278,7 @@ function defaultImageFixturePath() {
|
|
|
43285
44278
|
const fixtureDir = join4(dirname2(fileURLToPath(import.meta.url)), "..", "fixtures");
|
|
43286
44279
|
const fixturePath = join4(fixtureDir, "brandenburger-tor.png");
|
|
43287
44280
|
if (existsSync6(fixturePath)) return fixturePath;
|
|
43288
|
-
const tempDir =
|
|
44281
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "openmates-benchmark-"));
|
|
43289
44282
|
const tempPath = join4(tempDir, "brandenburger-tor.svg");
|
|
43290
44283
|
writeFileSync4(tempPath, FIXTURE_IMAGE_SVG, "utf-8");
|
|
43291
44284
|
return tempPath;
|
|
@@ -44804,6 +45797,9 @@ async function handleEmbeds(client, subcommand, rest, flags) {
|
|
|
44804
45797
|
var SETTINGS_EXECUTABLE_COMMANDS = [
|
|
44805
45798
|
{ path: ["account", "info"], description: "Show account info", examples: ["openmates settings account info --json"] },
|
|
44806
45799
|
{ path: ["account", "timezone", "set"], description: "Set account timezone", examples: ["openmates settings account timezone set Europe/Berlin"] },
|
|
45800
|
+
{ path: ["account", "interests", "list"], description: "Show encrypted account topic interests", examples: ["openmates settings account interests list --json"] },
|
|
45801
|
+
{ path: ["account", "interests", "set"], description: "Set encrypted account topic interests", examples: ["openmates settings account interests set software_development use_the_cli"] },
|
|
45802
|
+
{ path: ["account", "interests", "clear"], description: "Clear encrypted account topic interests", examples: ["openmates settings account interests clear --yes"] },
|
|
44807
45803
|
{ path: ["account", "export", "manifest"], description: "Show account export manifest", examples: ["openmates settings account export manifest --json"] },
|
|
44808
45804
|
{ path: ["account", "export", "data"], description: "Fetch account export data", examples: ["openmates settings account export data --json"] },
|
|
44809
45805
|
{ path: ["account", "import-chat"], description: "Import a CLI chat export file", examples: ["openmates settings account import-chat ./chat.yml", "openmates settings account import-chat ./payload.json"] },
|
|
@@ -44905,6 +45901,30 @@ async function printSettingsMutationResult(resultPromise, flags) {
|
|
|
44905
45901
|
process.stdout.write("\x1B[32m\u2713\x1B[0m Settings updated\n");
|
|
44906
45902
|
if (result && typeof result === "object") printGenericObject(result);
|
|
44907
45903
|
}
|
|
45904
|
+
function printTopicPreferences(preferences, flags, successLabel) {
|
|
45905
|
+
const payload = preferences ?? {
|
|
45906
|
+
version: 1,
|
|
45907
|
+
selectedTagIds: [],
|
|
45908
|
+
updatedAt: null
|
|
45909
|
+
};
|
|
45910
|
+
const result = {
|
|
45911
|
+
...payload,
|
|
45912
|
+
availableTagIds: INTEREST_TAG_IDS
|
|
45913
|
+
};
|
|
45914
|
+
if (flags.json === true) {
|
|
45915
|
+
printJson2(result);
|
|
45916
|
+
return;
|
|
45917
|
+
}
|
|
45918
|
+
if (successLabel) {
|
|
45919
|
+
process.stdout.write(`\x1B[32m\u2713\x1B[0m ${successLabel}
|
|
45920
|
+
`);
|
|
45921
|
+
}
|
|
45922
|
+
const selected = result.selectedTagIds.length > 0 ? result.selectedTagIds.join(", ") : "none";
|
|
45923
|
+
process.stdout.write(`Selected interests: ${selected}
|
|
45924
|
+
`);
|
|
45925
|
+
process.stdout.write(`Available interests: ${INTEREST_TAG_IDS.join(", ")}
|
|
45926
|
+
`);
|
|
45927
|
+
}
|
|
44908
45928
|
function printReportIssueCreateResult(result, flags) {
|
|
44909
45929
|
if (flags.json === true) {
|
|
44910
45930
|
printJson2(result);
|
|
@@ -45428,6 +46448,30 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
45428
46448
|
);
|
|
45429
46449
|
return;
|
|
45430
46450
|
}
|
|
46451
|
+
if (matches(tokens, ["account", "interests", "list"])) {
|
|
46452
|
+
const preferences = await client.getTopicPreferences();
|
|
46453
|
+
printTopicPreferences(preferences, flags);
|
|
46454
|
+
return;
|
|
46455
|
+
}
|
|
46456
|
+
if (matches(tokens, ["account", "interests", "set"])) {
|
|
46457
|
+
const selectedTagIds = rest.slice(2);
|
|
46458
|
+
if (selectedTagIds.length === 0) {
|
|
46459
|
+
throw new Error(
|
|
46460
|
+
`Missing interest tag IDs. Use one or more of: ${INTEREST_TAG_IDS.join(", ")}`
|
|
46461
|
+
);
|
|
46462
|
+
}
|
|
46463
|
+
const preferences = await client.setTopicPreferences(selectedTagIds);
|
|
46464
|
+
printTopicPreferences(preferences, flags, "Interests updated");
|
|
46465
|
+
return;
|
|
46466
|
+
}
|
|
46467
|
+
if (matches(tokens, ["account", "interests", "clear"])) {
|
|
46468
|
+
if (flags.yes !== true) {
|
|
46469
|
+
await confirmOrExit("Clear account interests? [y/N] ");
|
|
46470
|
+
}
|
|
46471
|
+
const preferences = await client.clearTopicPreferences();
|
|
46472
|
+
printTopicPreferences(preferences, flags, "Interests cleared");
|
|
46473
|
+
return;
|
|
46474
|
+
}
|
|
45431
46475
|
if (matches(tokens, ["account", "export", "manifest"])) {
|
|
45432
46476
|
await printSettingsResult(client.settingsGet("export-account-manifest"), flags);
|
|
45433
46477
|
return;
|
|
@@ -48323,6 +49367,8 @@ if (isCliEntrypoint()) {
|
|
|
48323
49367
|
}
|
|
48324
49368
|
|
|
48325
49369
|
export {
|
|
49370
|
+
INTEREST_TAG_IDS,
|
|
49371
|
+
normalizeInterestTagIds,
|
|
48326
49372
|
MEMORY_TYPE_REGISTRY,
|
|
48327
49373
|
MATE_NAMES,
|
|
48328
49374
|
deriveAppUrl,
|