openmates 0.12.0-alpha.23 → 0.12.0-alpha.25
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-V7DUGDYQ.js} +1138 -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,185 @@ 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 verifyChecksums(rootDir) {
|
|
9609
|
+
const checksumsPath = join3(rootDir, "checksums.sha256");
|
|
9610
|
+
if (!existsSync5(checksumsPath)) throw new Error("Backup archive is missing checksums.sha256.");
|
|
9611
|
+
for (const line of readFileSync5(checksumsPath, "utf-8").split("\n")) {
|
|
9612
|
+
const trimmed = line.trim();
|
|
9613
|
+
if (!trimmed) continue;
|
|
9614
|
+
const match = trimmed.match(/^([a-f0-9]{64}) {2}(.+)$/);
|
|
9615
|
+
if (!match) throw new Error(`Invalid checksum entry: ${trimmed}`);
|
|
9616
|
+
const relative2 = match[2];
|
|
9617
|
+
if (relative2.startsWith("/") || relative2.split(/[\\/]/).includes("..")) {
|
|
9618
|
+
throw new Error(`Unsafe checksum path in backup archive: ${relative2}`);
|
|
9619
|
+
}
|
|
9620
|
+
const filePath = join3(rootDir, relative2);
|
|
9621
|
+
if (!existsSync5(filePath)) throw new Error(`Backup archive is missing checksummed file: ${relative2}`);
|
|
9622
|
+
const actual = createHash5("sha256").update(readFileSync5(filePath)).digest("hex");
|
|
9623
|
+
if (actual !== match[1]) throw new Error(`Backup checksum mismatch for ${relative2}.`);
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
function createServerBackup(installPath, role, options = {}) {
|
|
9627
|
+
const plan = planBackup({ role, includeObservability: options.includeObservability });
|
|
9628
|
+
const backupDir = roleBackupDir(installPath, role);
|
|
9629
|
+
mkdirSync3(backupDir, { recursive: true, mode: 448 });
|
|
9630
|
+
const archivePath = options.output ? resolve3(options.output) : join3(backupDir, options.preUpdate ? `latest-pre-update-${role}.tar.gz` : `openmates-${role}-${nowStamp()}.tar.gz`);
|
|
9631
|
+
const tempDir = mkdtempSync(join3(backupDir, ".tmp-"));
|
|
9632
|
+
const env = readEnvMap(installPath);
|
|
9633
|
+
const manifest = {
|
|
9634
|
+
role,
|
|
9635
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9636
|
+
cli_version: getPackageVersion(),
|
|
9637
|
+
image_tag: getEnvVar(existsSync5(join3(installPath, ".env")) ? readFileSync5(join3(installPath, ".env"), "utf-8") : "", "OPENMATES_IMAGE_TAG"),
|
|
9638
|
+
include_observability: options.includeObservability === true,
|
|
9639
|
+
contents: plan.contents
|
|
9640
|
+
};
|
|
9641
|
+
try {
|
|
9642
|
+
writeFileSync3(join3(tempDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
9643
|
+
`);
|
|
9644
|
+
copyIfExists(join3(installPath, ".env"), join3(tempDir, "runtime", ".env"));
|
|
9645
|
+
copyIfExists(join3(installPath, "config"), join3(tempDir, "runtime", "config"));
|
|
9646
|
+
if (role === "core") {
|
|
9647
|
+
const databaseUser = env.DATABASE_USERNAME || "directus";
|
|
9648
|
+
const databaseName = env.DATABASE_NAME || "directus";
|
|
9649
|
+
try {
|
|
9650
|
+
const dump = execSync(
|
|
9651
|
+
`docker exec cms-database pg_dump --clean --if-exists --no-owner --no-privileges -U ${shellQuote(databaseUser)} ${shellQuote(databaseName)}`,
|
|
9652
|
+
{ encoding: "utf-8" }
|
|
9653
|
+
);
|
|
9654
|
+
writeFileSync3(join3(tempDir, "postgres.sql"), dump);
|
|
9655
|
+
} catch (error) {
|
|
9656
|
+
throw new Error(`Postgres backup failed. Is cms-database running? ${error instanceof Error ? error.message : String(error)}`);
|
|
9657
|
+
}
|
|
9658
|
+
}
|
|
9659
|
+
for (const item of [
|
|
9660
|
+
[join3(installPath, "backend", "core", "uploads"), join3(tempDir, "directus-uploads")],
|
|
9661
|
+
[join3(installPath, "backend", "core", "extensions"), join3(tempDir, "directus-extensions")],
|
|
9662
|
+
[join3(installPath, "backend", role, "vault"), join3(tempDir, `${role}-vault-config`)]
|
|
9663
|
+
]) {
|
|
9664
|
+
copyIfExists(item[0], item[1]);
|
|
9665
|
+
}
|
|
9666
|
+
writeChecksums(tempDir);
|
|
9667
|
+
execSync(`tar -czf ${shellQuote(archivePath)} -C ${shellQuote(tempDir)} .`, { stdio: "pipe" });
|
|
9668
|
+
chmodSync2(archivePath, plan.fileMode);
|
|
9669
|
+
return archivePath;
|
|
9670
|
+
} finally {
|
|
9671
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
function restoreServerBackup(installPath, role, file) {
|
|
9675
|
+
const archivePath = resolve3(file);
|
|
9676
|
+
if (!existsSync5(archivePath)) throw new Error(`Backup file not found: ${archivePath}`);
|
|
9677
|
+
mkdirSync3(roleBackupDir(installPath, role), { recursive: true, mode: 448 });
|
|
9678
|
+
const tempDir = mkdtempSync(join3(roleBackupDir(installPath, role), ".restore-"));
|
|
9679
|
+
const env = readEnvMap(installPath);
|
|
9680
|
+
try {
|
|
9681
|
+
execSync(`tar -xzf ${shellQuote(archivePath)} -C ${shellQuote(tempDir)}`, { stdio: "pipe" });
|
|
9682
|
+
verifyChecksums(tempDir);
|
|
9683
|
+
const manifestPath = join3(tempDir, "manifest.json");
|
|
9684
|
+
if (!existsSync5(manifestPath)) throw new Error("Backup archive is missing manifest.json.");
|
|
9685
|
+
const manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
9686
|
+
if (manifest.role !== role) throw new Error(`Backup role '${manifest.role}' does not match requested role '${role}'.`);
|
|
9687
|
+
copyIfExists(join3(tempDir, "runtime", ".env"), join3(installPath, ".env"));
|
|
9688
|
+
copyIfExists(join3(tempDir, "runtime", "config"), join3(installPath, "config"));
|
|
9689
|
+
const postgresDump = join3(tempDir, "postgres.sql");
|
|
9690
|
+
if (role === "core" && existsSync5(postgresDump)) {
|
|
9691
|
+
const databaseUser = env.DATABASE_USERNAME || "directus";
|
|
9692
|
+
const databaseName = env.DATABASE_NAME || "directus";
|
|
9693
|
+
execSync(`docker exec -i cms-database psql -v ON_ERROR_STOP=1 -U ${shellQuote(databaseUser)} ${shellQuote(databaseName)}`, {
|
|
9694
|
+
input: readFileSync5(postgresDump),
|
|
9695
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
9696
|
+
});
|
|
9697
|
+
}
|
|
9698
|
+
} finally {
|
|
9699
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
9702
|
+
function restoreStopServices(role) {
|
|
9703
|
+
if (role !== "core") return [];
|
|
9704
|
+
return resolveServiceSelection("core", { exclude: "cms-database" });
|
|
9705
|
+
}
|
|
9107
9706
|
async function promptText(question, defaultValue = "") {
|
|
9108
9707
|
const rl = createPromptInterface({ input: process.stdin, output: process.stderr });
|
|
9109
9708
|
try {
|
|
@@ -9200,11 +9799,13 @@ async function serverStatus(flags) {
|
|
|
9200
9799
|
const installPath = resolveServerPath(flags);
|
|
9201
9800
|
ensureGitWorkDirEnv(installPath);
|
|
9202
9801
|
const config = loadConfigForInstallPath(installPath);
|
|
9802
|
+
const role = getServerRole(flags, config);
|
|
9203
9803
|
const withOverrides = config?.composeProfile === "full";
|
|
9204
|
-
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "ps"];
|
|
9804
|
+
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "ps"];
|
|
9205
9805
|
if (flags.json === true) {
|
|
9206
9806
|
args.push("--format", "json");
|
|
9207
9807
|
}
|
|
9808
|
+
if (hasServiceFilter(flags)) args.push(...selectedComposeServices(role, flags));
|
|
9208
9809
|
const code = await runInteractive("docker", args, installPath);
|
|
9209
9810
|
if (code !== 0) process.exit(code);
|
|
9210
9811
|
}
|
|
@@ -9215,9 +9816,15 @@ async function serverStart(flags) {
|
|
|
9215
9816
|
warnIfMissingLlmCredentials(installPath);
|
|
9216
9817
|
const withOverrides = flags["with-overrides"] === true;
|
|
9217
9818
|
const config = loadConfigForInstallPath(installPath);
|
|
9819
|
+
const role = getServerRole(flags, config);
|
|
9218
9820
|
const installMode = getInstallMode(installPath, config);
|
|
9219
|
-
const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
|
|
9220
|
-
const args = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
9821
|
+
const pullArgs = [...composeArgs(installPath, withOverrides, installMode, role), "pull"];
|
|
9822
|
+
const args = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9823
|
+
if (hasServiceFilter(flags)) {
|
|
9824
|
+
const services = selectedComposeServices(role, flags);
|
|
9825
|
+
pullArgs.push(...services);
|
|
9826
|
+
args.push(...services);
|
|
9827
|
+
}
|
|
9221
9828
|
if (config && withOverrides && config.composeProfile !== "full") {
|
|
9222
9829
|
saveServerConfig({ ...config, composeProfile: "full" });
|
|
9223
9830
|
}
|
|
@@ -9246,8 +9853,9 @@ async function serverStop(flags) {
|
|
|
9246
9853
|
const installPath = resolveServerPath(flags);
|
|
9247
9854
|
ensureGitWorkDirEnv(installPath);
|
|
9248
9855
|
const config = loadConfigForInstallPath(installPath);
|
|
9856
|
+
const role = getServerRole(flags, config);
|
|
9249
9857
|
const withOverrides = config?.composeProfile === "full";
|
|
9250
|
-
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "down"];
|
|
9858
|
+
const args = hasServiceFilter(flags) ? [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "stop", ...selectedComposeServices(role, flags)] : [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "down"];
|
|
9251
9859
|
console.error("Stopping OpenMates server...");
|
|
9252
9860
|
const code = await runInteractive("docker", args, installPath);
|
|
9253
9861
|
if (code !== 0) process.exit(code);
|
|
@@ -9262,31 +9870,36 @@ async function serverRestart(flags) {
|
|
|
9262
9870
|
const installPath = resolveServerPath(flags);
|
|
9263
9871
|
ensureGitWorkDirEnv(installPath);
|
|
9264
9872
|
const config = loadConfigForInstallPath(installPath);
|
|
9873
|
+
const role = getServerRole(flags, config);
|
|
9265
9874
|
const withOverrides = config?.composeProfile === "full";
|
|
9266
9875
|
const installMode = getInstallMode(installPath, config);
|
|
9267
9876
|
if (flags.rebuild === true) {
|
|
9877
|
+
if (hasServiceFilter(flags)) {
|
|
9878
|
+
throw new Error("--services/--exclude cannot be combined with --rebuild. Use graceful restart for service-scoped restarts.");
|
|
9879
|
+
}
|
|
9268
9880
|
if (installMode === "image") {
|
|
9269
9881
|
throw new Error(
|
|
9270
9882
|
"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
9883
|
);
|
|
9272
9884
|
}
|
|
9273
9885
|
console.error("Rebuilding OpenMates server (this may take a few minutes)...");
|
|
9274
|
-
const downArgs = [...composeArgs(installPath, withOverrides, installMode), "down"];
|
|
9886
|
+
const downArgs = [...composeArgs(installPath, withOverrides, installMode, role), "down"];
|
|
9275
9887
|
let code = await runInteractive("docker", downArgs, installPath);
|
|
9276
9888
|
if (code !== 0) process.exit(code);
|
|
9277
9889
|
try {
|
|
9278
9890
|
exec("docker volume rm openmates-cache-data", installPath);
|
|
9279
9891
|
} catch {
|
|
9280
9892
|
}
|
|
9281
|
-
const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
|
|
9893
|
+
const buildArgs = [...composeArgs(installPath, withOverrides, installMode, role), "build"];
|
|
9282
9894
|
code = await runInteractive("docker", buildArgs, installPath);
|
|
9283
9895
|
if (code !== 0) process.exit(code);
|
|
9284
|
-
const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
9896
|
+
const upArgs = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9285
9897
|
code = await runInteractive("docker", upArgs, installPath);
|
|
9286
9898
|
if (code !== 0) process.exit(code);
|
|
9287
9899
|
} else {
|
|
9288
9900
|
console.error("Restarting OpenMates server...");
|
|
9289
|
-
const args = [...composeArgs(installPath, withOverrides, installMode), "restart"];
|
|
9901
|
+
const args = [...composeArgs(installPath, withOverrides, installMode, role), "restart"];
|
|
9902
|
+
if (hasServiceFilter(flags)) args.push(...selectedComposeServices(role, flags));
|
|
9290
9903
|
const code = await runInteractive("docker", args, installPath);
|
|
9291
9904
|
if (code !== 0) process.exit(code);
|
|
9292
9905
|
}
|
|
@@ -9301,8 +9914,9 @@ async function serverLogs(flags) {
|
|
|
9301
9914
|
const installPath = resolveServerPath(flags);
|
|
9302
9915
|
ensureGitWorkDirEnv(installPath);
|
|
9303
9916
|
const config = loadConfigForInstallPath(installPath);
|
|
9917
|
+
const role = getServerRole(flags, config);
|
|
9304
9918
|
const withOverrides = config?.composeProfile === "full";
|
|
9305
|
-
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "logs"];
|
|
9919
|
+
const args = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "logs"];
|
|
9306
9920
|
if (flags.follow === true || flags.f === true) {
|
|
9307
9921
|
args.push("--follow");
|
|
9308
9922
|
}
|
|
@@ -9312,9 +9926,13 @@ async function serverLogs(flags) {
|
|
|
9312
9926
|
} else {
|
|
9313
9927
|
args.push("--tail", "100");
|
|
9314
9928
|
}
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9929
|
+
if (hasServiceFilter(flags)) {
|
|
9930
|
+
args.push(...selectedComposeServices(role, flags));
|
|
9931
|
+
} else {
|
|
9932
|
+
const container = flags.container;
|
|
9933
|
+
if (typeof container === "string") {
|
|
9934
|
+
args.push(container);
|
|
9935
|
+
}
|
|
9318
9936
|
}
|
|
9319
9937
|
const code = await runInteractive("docker", args, installPath);
|
|
9320
9938
|
if (code !== 0) process.exit(code);
|
|
@@ -9323,7 +9941,10 @@ async function serverInstall(flags) {
|
|
|
9323
9941
|
const installPath = typeof flags.path === "string" ? resolve3(flags.path) : DEFAULT_INSTALL_PATH;
|
|
9324
9942
|
const sourcePath = typeof flags["source-path"] === "string" ? resolve3(flags["source-path"]) : null;
|
|
9325
9943
|
const fromSource = flags["from-source"] === true || sourcePath !== null;
|
|
9326
|
-
|
|
9944
|
+
const role = getServerRole(flags, null);
|
|
9945
|
+
const profile = getCoreProfile(flags, null);
|
|
9946
|
+
const runtimePlan = planServerRuntime({ role, profile, withAlerts: flags["with-alerts"] === true });
|
|
9947
|
+
if (existsSync5(join3(installPath, SOURCE_COMPOSE_FILE)) || Object.values(ROLE_IMAGE_COMPOSE_FILES).some((composeFile) => existsSync5(join3(installPath, composeFile)))) {
|
|
9327
9948
|
console.error(`OpenMates already exists at ${installPath}.`);
|
|
9328
9949
|
console.error("Use 'openmates server update' to update, or choose a different --path.");
|
|
9329
9950
|
process.exit(1);
|
|
@@ -9341,7 +9962,7 @@ async function serverInstall(flags) {
|
|
|
9341
9962
|
}
|
|
9342
9963
|
const imageTag = typeof flags["image-tag"] === "string" ? flags["image-tag"] : getDefaultImageTag();
|
|
9343
9964
|
console.error(`Preparing OpenMates image-mode install at ${installPath}...`);
|
|
9344
|
-
await writeImageModeRuntimeFiles(installPath, imageTag);
|
|
9965
|
+
await writeImageModeRuntimeFiles(installPath, imageTag, role);
|
|
9345
9966
|
const cliUrls2 = deriveSelfHostCliUrls(readFileSync5(join3(installPath, ".env"), "utf-8"));
|
|
9346
9967
|
try {
|
|
9347
9968
|
exec("docker network create openmates", installPath);
|
|
@@ -9350,7 +9971,11 @@ async function serverInstall(flags) {
|
|
|
9350
9971
|
saveServerConfig({
|
|
9351
9972
|
installPath,
|
|
9352
9973
|
installedAt: Date.now(),
|
|
9353
|
-
composeProfile: "core",
|
|
9974
|
+
composeProfile: role === "core" ? "core" : "core",
|
|
9975
|
+
serverRole: role,
|
|
9976
|
+
serverProfile: profile,
|
|
9977
|
+
defaultServices: runtimePlan.defaultServices,
|
|
9978
|
+
composeFiles: runtimePlan.composeFiles,
|
|
9354
9979
|
installMode: "image",
|
|
9355
9980
|
imageTag,
|
|
9356
9981
|
...cliUrls2
|
|
@@ -9417,6 +10042,10 @@ CLI default API: ${cliUrls2.apiUrl}`);
|
|
|
9417
10042
|
installPath,
|
|
9418
10043
|
installedAt: Date.now(),
|
|
9419
10044
|
composeProfile: "core",
|
|
10045
|
+
serverRole: role,
|
|
10046
|
+
serverProfile: profile,
|
|
10047
|
+
defaultServices: runtimePlan.defaultServices,
|
|
10048
|
+
composeFiles: runtimePlan.composeFiles,
|
|
9420
10049
|
installMode: "source",
|
|
9421
10050
|
...cliUrls
|
|
9422
10051
|
});
|
|
@@ -9434,13 +10063,71 @@ CLI default API: ${cliUrls.apiUrl}`);
|
|
|
9434
10063
|
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
10064
|
}
|
|
9436
10065
|
}
|
|
9437
|
-
async function
|
|
10066
|
+
async function installContinuousUpdateService(flags) {
|
|
10067
|
+
const config = loadServerConfig();
|
|
10068
|
+
const role = getServerRole(flags, config);
|
|
10069
|
+
const channel = typeof flags.channel === "string" ? flags.channel : config?.imageChannel ?? "main";
|
|
10070
|
+
const window = typeof flags.window === "string" ? flags.window : "02:00-04:00 UTC";
|
|
10071
|
+
const plan = planContinuousUpdateService({ role, channel, window });
|
|
10072
|
+
const servicePath = join3("/etc", "systemd", "system", plan.serviceName);
|
|
10073
|
+
const timerPath = join3("/etc", "systemd", "system", plan.timerName);
|
|
10074
|
+
if (flags["dry-run"] === true || flags.json === true) {
|
|
10075
|
+
printJson({ command: "update install-service", status: "planned", role, servicePath, timerPath, unit: plan.unit, timer: plan.timer });
|
|
10076
|
+
return;
|
|
10077
|
+
}
|
|
10078
|
+
try {
|
|
10079
|
+
writeFileSync3(servicePath, plan.unit, { mode: 420 });
|
|
10080
|
+
writeFileSync3(timerPath, plan.timer, { mode: 420 });
|
|
10081
|
+
execSync("systemctl daemon-reload", { stdio: "pipe" });
|
|
10082
|
+
execSync(`systemctl enable --now ${shellQuote(plan.timerName)}`, { stdio: "pipe" });
|
|
10083
|
+
} catch (error) {
|
|
10084
|
+
throw new Error(
|
|
10085
|
+
`Could not install systemd updater. Run with sudo or use --dry-run to inspect generated units. ${error instanceof Error ? error.message : String(error)}`
|
|
10086
|
+
);
|
|
10087
|
+
}
|
|
10088
|
+
console.log(`Installed ${plan.timerName}.`);
|
|
10089
|
+
}
|
|
10090
|
+
async function serverUpdate(rest, flags) {
|
|
10091
|
+
if (rest[0] === "status") {
|
|
10092
|
+
const installPath2 = resolveServerPath(flags);
|
|
10093
|
+
const config2 = loadConfigForInstallPath(installPath2);
|
|
10094
|
+
const role2 = getServerRole(flags, config2);
|
|
10095
|
+
const filePath = updateStatusFile(installPath2, role2);
|
|
10096
|
+
if (flags.json === true) {
|
|
10097
|
+
printJson(existsSync5(filePath) ? JSON.parse(readFileSync5(filePath, "utf-8")) : { role: role2, status: "unknown" });
|
|
10098
|
+
return;
|
|
10099
|
+
}
|
|
10100
|
+
if (!existsSync5(filePath)) {
|
|
10101
|
+
console.log(`No update status recorded for ${role2}.`);
|
|
10102
|
+
return;
|
|
10103
|
+
}
|
|
10104
|
+
console.log(readFileSync5(filePath, "utf-8").trim());
|
|
10105
|
+
return;
|
|
10106
|
+
}
|
|
10107
|
+
if (rest[0] === "install-service") {
|
|
10108
|
+
if (flags.continuous !== true) throw new Error("Usage: openmates server update install-service --continuous [--channel main|dev|stable]");
|
|
10109
|
+
await installContinuousUpdateService(flags);
|
|
10110
|
+
return;
|
|
10111
|
+
}
|
|
10112
|
+
if (flags.continuous === true) {
|
|
10113
|
+
const intervalMinutes = typeof flags.interval === "string" ? Number.parseInt(flags.interval, 10) : 30;
|
|
10114
|
+
if (!Number.isFinite(intervalMinutes) || intervalMinutes < 5) throw new Error("--interval must be at least 5 minutes.");
|
|
10115
|
+
console.error(`Running continuous updater every ${intervalMinutes} minutes. Use Ctrl+C to stop.`);
|
|
10116
|
+
while (true) {
|
|
10117
|
+
await serverUpdate([], { ...flags, continuous: false });
|
|
10118
|
+
await sleep2(intervalMinutes * 6e4);
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
9438
10121
|
const installPath = resolveServerPath(flags);
|
|
9439
10122
|
const dryRun = flags["dry-run"] === true;
|
|
9440
10123
|
if (!dryRun) ensureGitWorkDirEnv(installPath);
|
|
9441
10124
|
const config = loadConfigForInstallPath(installPath);
|
|
10125
|
+
const role = getServerRole(flags, config);
|
|
9442
10126
|
const withOverrides = config?.composeProfile === "full";
|
|
9443
10127
|
const installMode = getInstallMode(installPath, config);
|
|
10128
|
+
const filterRequested = hasServiceFilter(flags);
|
|
10129
|
+
const selectedServices = filterRequested ? selectedComposeServices(role, flags) : [];
|
|
10130
|
+
const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
|
|
9444
10131
|
if (installMode === "source" && (flags["image-tag"] !== void 0 || flags.channel !== void 0)) {
|
|
9445
10132
|
throw new Error("--image-tag and --channel only apply to image-mode installs. Source-mode installs update from Git.");
|
|
9446
10133
|
}
|
|
@@ -9450,14 +10137,22 @@ async function serverUpdate(flags) {
|
|
|
9450
10137
|
const currentTag = getImageTagFromEnv(installPath, config);
|
|
9451
10138
|
const target = resolveTargetImageTag(flags, currentTag, getPackageVersion());
|
|
9452
10139
|
const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
|
|
10140
|
+
const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingEnvKeys });
|
|
9453
10141
|
const plan = {
|
|
9454
10142
|
command: "update",
|
|
10143
|
+
role,
|
|
9455
10144
|
path: installPath,
|
|
9456
10145
|
mode: "image",
|
|
9457
10146
|
currentImageTag: currentTag || null,
|
|
9458
10147
|
targetImageTag: target.tag,
|
|
9459
10148
|
channel: target.channel ?? null,
|
|
9460
10149
|
templateRef,
|
|
10150
|
+
selectedServices: filterRequested ? selectedServices : "all",
|
|
10151
|
+
steps: safetyPlan.steps,
|
|
10152
|
+
backupName: safetyPlan.backupName,
|
|
10153
|
+
missingRequiredEnvKeys: missingEnvKeys,
|
|
10154
|
+
blocked: safetyPlan.blocked,
|
|
10155
|
+
blockReason: safetyPlan.blockReason,
|
|
9461
10156
|
dryRun
|
|
9462
10157
|
};
|
|
9463
10158
|
if (dryRun) {
|
|
@@ -9469,37 +10164,59 @@ async function serverUpdate(flags) {
|
|
|
9469
10164
|
console.log(` Current tag: ${currentTag || "unknown"}`);
|
|
9470
10165
|
console.log(` Target tag: ${target.tag}`);
|
|
9471
10166
|
console.log(` Template ref: ${templateRef}`);
|
|
10167
|
+
console.log(` Role: ${role}`);
|
|
10168
|
+
console.log(` Services: ${filterRequested ? selectedServices.join(", ") : "all"}`);
|
|
10169
|
+
console.log(` Backup: ${safetyPlan.backupName ?? "none"}`);
|
|
10170
|
+
console.log(` Steps: ${safetyPlan.steps.join(" -> ")}`);
|
|
10171
|
+
console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
|
|
9472
10172
|
console.log(" Commands: refresh compose, docker compose pull, docker compose up -d, health checks");
|
|
9473
10173
|
}
|
|
9474
10174
|
return;
|
|
9475
10175
|
}
|
|
10176
|
+
if (safetyPlan.blocked) throw new Error(safetyPlan.blockReason ?? "Update blocked by preflight.");
|
|
10177
|
+
if (missingEnvKeys.length && flags.yes !== true) {
|
|
10178
|
+
throw new Error(`Required environment keys are missing: ${missingEnvKeys.join(", ")}. Add them to .env or rerun with --yes after reviewing.`);
|
|
10179
|
+
}
|
|
9476
10180
|
console.error(`Mode: image`);
|
|
9477
10181
|
console.error(`Current image tag: ${currentTag || "unknown"}`);
|
|
9478
10182
|
console.error(`Target image tag: ${target.tag}`);
|
|
10183
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "backup" });
|
|
10184
|
+
if (safetyPlan.backupName) {
|
|
10185
|
+
console.error(`Creating rotating pre-update backup: ${safetyPlan.backupName}`);
|
|
10186
|
+
createServerBackup(installPath, role, { preUpdate: true });
|
|
10187
|
+
} else {
|
|
10188
|
+
console.error("Skipping pre-update backup for this role or because --skip-backup was passed.");
|
|
10189
|
+
}
|
|
9479
10190
|
console.error(`Refreshing self-host runtime files from ${templateRef}...`);
|
|
9480
|
-
await writeImageModeRuntimeFiles(installPath, target.tag);
|
|
9481
|
-
const pullArgs = [...composeArgs(installPath, withOverrides, installMode), "pull"];
|
|
10191
|
+
await writeImageModeRuntimeFiles(installPath, target.tag, role);
|
|
10192
|
+
const pullArgs = [...composeArgs(installPath, withOverrides, installMode, role), "pull"];
|
|
10193
|
+
if (filterRequested) pullArgs.push(...selectedServices);
|
|
9482
10194
|
let code2 = 0;
|
|
9483
10195
|
if (shouldPullImages()) {
|
|
10196
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "pull" });
|
|
9484
10197
|
console.error("Pulling prebuilt images...");
|
|
9485
10198
|
code2 = await runInteractive("docker", pullArgs, installPath);
|
|
9486
10199
|
if (code2 !== 0) process.exit(code2);
|
|
9487
10200
|
} else {
|
|
9488
10201
|
console.error("Skipping image pull because OPENMATES_SKIP_IMAGE_PULL=1.");
|
|
9489
10202
|
}
|
|
9490
|
-
|
|
10203
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "up" });
|
|
10204
|
+
const upArgs2 = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
10205
|
+
if (filterRequested) upArgs2.push(...selectedServices);
|
|
9491
10206
|
code2 = await runInteractive("docker", upArgs2, installPath);
|
|
9492
10207
|
if (code2 !== 0) process.exit(code2);
|
|
9493
|
-
console.error("Waiting for
|
|
10208
|
+
console.error("Waiting for role health checks...");
|
|
9494
10209
|
try {
|
|
9495
|
-
|
|
10210
|
+
writeUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, step: "health-check" });
|
|
10211
|
+
await waitForServerHealth(installPath, role);
|
|
9496
10212
|
} catch (error) {
|
|
9497
|
-
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode), "ps"], installPath);
|
|
10213
|
+
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "ps"], installPath);
|
|
9498
10214
|
throw error;
|
|
9499
10215
|
}
|
|
9500
10216
|
if (config) {
|
|
9501
10217
|
saveServerConfig({ ...config, imageTag: target.tag, imageChannel: target.channel });
|
|
9502
10218
|
}
|
|
10219
|
+
writeUpdateStatus(installPath, role, { status: "success", targetImageTag: target.tag, step: "complete" });
|
|
9503
10220
|
if (flags.json === true) {
|
|
9504
10221
|
printJson({ ...plan, status: "success", dryRun: false });
|
|
9505
10222
|
} else {
|
|
@@ -9545,18 +10262,18 @@ async function serverUpdate(flags) {
|
|
|
9545
10262
|
} catch {
|
|
9546
10263
|
}
|
|
9547
10264
|
}
|
|
9548
|
-
const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
|
|
10265
|
+
const buildArgs = [...composeArgs(installPath, withOverrides, installMode, role), "build"];
|
|
9549
10266
|
console.error("Rebuilding containers...");
|
|
9550
10267
|
let code = await runInteractive("docker", buildArgs, installPath);
|
|
9551
10268
|
if (code !== 0) process.exit(code);
|
|
9552
|
-
const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
10269
|
+
const upArgs = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9553
10270
|
code = await runInteractive("docker", upArgs, installPath);
|
|
9554
10271
|
if (code !== 0) process.exit(code);
|
|
9555
10272
|
console.error("Waiting for API and web health checks...");
|
|
9556
10273
|
try {
|
|
9557
|
-
await waitForServerHealth(installPath);
|
|
10274
|
+
await waitForServerHealth(installPath, role);
|
|
9558
10275
|
} catch (error) {
|
|
9559
|
-
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode), "ps"], installPath);
|
|
10276
|
+
await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "ps"], installPath);
|
|
9560
10277
|
throw error;
|
|
9561
10278
|
}
|
|
9562
10279
|
if (flags.json === true) {
|
|
@@ -9570,6 +10287,7 @@ async function serverReset(flags) {
|
|
|
9570
10287
|
const installPath = resolveServerPath(flags);
|
|
9571
10288
|
ensureGitWorkDirEnv(installPath);
|
|
9572
10289
|
const config = loadConfigForInstallPath(installPath);
|
|
10290
|
+
const role = getServerRole(flags, config);
|
|
9573
10291
|
const withOverrides = config?.composeProfile === "full";
|
|
9574
10292
|
const installMode = getInstallMode(installPath, config);
|
|
9575
10293
|
const userDataOnly = flags["delete-user-data-only"] === true;
|
|
@@ -9591,7 +10309,7 @@ async function serverReset(flags) {
|
|
|
9591
10309
|
}
|
|
9592
10310
|
console.error("Resetting server...");
|
|
9593
10311
|
if (userDataOnly) {
|
|
9594
|
-
const downArgs = [...composeArgs(installPath, withOverrides, installMode), "down"];
|
|
10312
|
+
const downArgs = [...composeArgs(installPath, withOverrides, installMode, role), "down"];
|
|
9595
10313
|
let code = await runInteractive("docker", downArgs, installPath);
|
|
9596
10314
|
if (code !== 0) process.exit(code);
|
|
9597
10315
|
for (const vol of ["openmates-cache-data", "openmates-postgres-data", "openmates-cms-database-data"]) {
|
|
@@ -9602,15 +10320,15 @@ async function serverReset(flags) {
|
|
|
9602
10320
|
}
|
|
9603
10321
|
}
|
|
9604
10322
|
if (installMode === "source") {
|
|
9605
|
-
const buildArgs = [...composeArgs(installPath, withOverrides, installMode), "build"];
|
|
10323
|
+
const buildArgs = [...composeArgs(installPath, withOverrides, installMode, role), "build"];
|
|
9606
10324
|
code = await runInteractive("docker", buildArgs, installPath);
|
|
9607
10325
|
if (code !== 0) process.exit(code);
|
|
9608
10326
|
}
|
|
9609
|
-
const upArgs = [...composeArgs(installPath, withOverrides, installMode), "up", "-d"];
|
|
10327
|
+
const upArgs = [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"];
|
|
9610
10328
|
code = await runInteractive("docker", upArgs, installPath);
|
|
9611
10329
|
if (code !== 0) process.exit(code);
|
|
9612
10330
|
} else {
|
|
9613
|
-
const args = [...composeArgs(installPath, withOverrides, installMode), "down", "-v"];
|
|
10331
|
+
const args = [...composeArgs(installPath, withOverrides, installMode, role), "down", "-v"];
|
|
9614
10332
|
const code = await runInteractive("docker", args, installPath);
|
|
9615
10333
|
if (code !== 0) process.exit(code);
|
|
9616
10334
|
}
|
|
@@ -9888,11 +10606,76 @@ async function serverAi(rest, flags) {
|
|
|
9888
10606
|
throw new Error(`Unknown server ai models command '${action}'. Use add, list, test, or remove.`);
|
|
9889
10607
|
}
|
|
9890
10608
|
}
|
|
10609
|
+
async function serverBackup(rest, flags) {
|
|
10610
|
+
const action = rest[0];
|
|
10611
|
+
const installPath = resolveServerPath(flags);
|
|
10612
|
+
const config = loadConfigForInstallPath(installPath);
|
|
10613
|
+
const role = getServerRole(flags, config);
|
|
10614
|
+
if (action === "list") {
|
|
10615
|
+
const dir = roleBackupDir(installPath, role);
|
|
10616
|
+
const files = existsSync5(dir) ? readdirSync(dir).filter((item) => item.endsWith(".tar.gz")).sort() : [];
|
|
10617
|
+
if (flags.json === true) {
|
|
10618
|
+
printJson({ role, backupDir: dir, files });
|
|
10619
|
+
return;
|
|
10620
|
+
}
|
|
10621
|
+
console.log(`Backups for ${role}:`);
|
|
10622
|
+
if (!files.length) {
|
|
10623
|
+
console.log(" none");
|
|
10624
|
+
return;
|
|
10625
|
+
}
|
|
10626
|
+
for (const file of files) console.log(` ${join3(dir, file)}`);
|
|
10627
|
+
return;
|
|
10628
|
+
}
|
|
10629
|
+
requireDocker();
|
|
10630
|
+
const output = typeof flags.output === "string" ? flags.output : void 0;
|
|
10631
|
+
const archivePath = createServerBackup(installPath, role, {
|
|
10632
|
+
output,
|
|
10633
|
+
includeObservability: flags["include-observability"] === true
|
|
10634
|
+
});
|
|
10635
|
+
if (flags.json === true) {
|
|
10636
|
+
printJson({ command: "backup", status: "success", role, file: archivePath });
|
|
10637
|
+
} else {
|
|
10638
|
+
console.log(`Backup created: ${archivePath}`);
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
async function serverRestore(flags) {
|
|
10642
|
+
requireDocker();
|
|
10643
|
+
const installPath = resolveServerPath(flags);
|
|
10644
|
+
const config = loadConfigForInstallPath(installPath);
|
|
10645
|
+
const role = getServerRole(flags, config);
|
|
10646
|
+
const file = typeof flags.file === "string" ? flags.file : "";
|
|
10647
|
+
if (!file) throw new Error("Usage: openmates server restore --file <backup.tar.gz> [--role core|upload|preview] [--yes]");
|
|
10648
|
+
const restorePlan = planRestore({ role, file, yes: flags.yes === true });
|
|
10649
|
+
if (restorePlan.requiresConfirmation) {
|
|
10650
|
+
console.error(`
|
|
10651
|
+
WARNING: This will restore ${role} data from ${file}.`);
|
|
10652
|
+
const confirmed = await confirmDestructive("RESTORE OPENMATES BACKUP");
|
|
10653
|
+
if (!confirmed) {
|
|
10654
|
+
console.error("Restore cancelled.");
|
|
10655
|
+
return;
|
|
10656
|
+
}
|
|
10657
|
+
}
|
|
10658
|
+
const withOverrides = config?.composeProfile === "full";
|
|
10659
|
+
const installMode = getInstallMode(installPath, config);
|
|
10660
|
+
const stopArgs = [...composeArgs(installPath, withOverrides, installMode, role), "stop", ...restoreStopServices(role)];
|
|
10661
|
+
let code = await runInteractive("docker", stopArgs, installPath);
|
|
10662
|
+
if (code !== 0) process.exit(code);
|
|
10663
|
+
restoreServerBackup(installPath, role, file);
|
|
10664
|
+
code = await runInteractive("docker", [...composeArgs(installPath, withOverrides, installMode, role), "up", "-d"], installPath);
|
|
10665
|
+
if (code !== 0) process.exit(code);
|
|
10666
|
+
await waitForServerHealth(installPath, role);
|
|
10667
|
+
if (flags.json === true) {
|
|
10668
|
+
printJson({ command: "restore", status: "success", role, file });
|
|
10669
|
+
} else {
|
|
10670
|
+
console.log(`Restore completed for ${role}.`);
|
|
10671
|
+
}
|
|
10672
|
+
}
|
|
9891
10673
|
async function serverUninstall(flags) {
|
|
9892
10674
|
requireDocker();
|
|
9893
10675
|
const installPath = resolveServerPath(flags);
|
|
9894
10676
|
ensureGitWorkDirEnv(installPath);
|
|
9895
10677
|
const config = loadConfigForInstallPath(installPath);
|
|
10678
|
+
const role = getServerRole(flags, config);
|
|
9896
10679
|
const withOverrides = config?.composeProfile === "full";
|
|
9897
10680
|
const keepData = flags["keep-data"] === true;
|
|
9898
10681
|
console.error("\nWARNING: This will completely uninstall OpenMates:");
|
|
@@ -9914,7 +10697,7 @@ async function serverUninstall(flags) {
|
|
|
9914
10697
|
}
|
|
9915
10698
|
}
|
|
9916
10699
|
console.error("Uninstalling OpenMates...");
|
|
9917
|
-
const downArgs = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config)), "down", "--rmi", "local"];
|
|
10700
|
+
const downArgs = [...composeArgs(installPath, withOverrides, getInstallMode(installPath, config), role), "down", "--rmi", "local"];
|
|
9918
10701
|
if (!keepData) {
|
|
9919
10702
|
downArgs.push("-v");
|
|
9920
10703
|
}
|
|
@@ -9940,6 +10723,113 @@ async function serverUninstall(flags) {
|
|
|
9940
10723
|
}
|
|
9941
10724
|
}
|
|
9942
10725
|
}
|
|
10726
|
+
async function serverPreflight(flags) {
|
|
10727
|
+
const installPath = resolveServerPath(flags);
|
|
10728
|
+
const config = loadConfigForInstallPath(installPath);
|
|
10729
|
+
const role = getServerRole(flags, config);
|
|
10730
|
+
const services = hasServiceFilter(flags) ? selectedComposeServices(role, flags) : [];
|
|
10731
|
+
const updatePlan = planUpdate({ role, selectedServices: services, dryRun: true });
|
|
10732
|
+
const missingEnvKeys = missingRequiredEnvKeys(installPath, role);
|
|
10733
|
+
const runtimePlan = planServerRuntime({
|
|
10734
|
+
role,
|
|
10735
|
+
profile: getCoreProfile(flags, config),
|
|
10736
|
+
withAlerts: flags["with-alerts"] === true
|
|
10737
|
+
});
|
|
10738
|
+
const caddyPlan = planCaddyCommand({ role, action: "status" });
|
|
10739
|
+
const preflight = {
|
|
10740
|
+
command: "preflight",
|
|
10741
|
+
status: updatePlan.blocked ? "blocked" : "ok",
|
|
10742
|
+
path: installPath,
|
|
10743
|
+
role,
|
|
10744
|
+
profile: runtimePlan.profile,
|
|
10745
|
+
services: hasServiceFilter(flags) ? services : "all",
|
|
10746
|
+
healthChecks: runtimePlan.healthChecks,
|
|
10747
|
+
updateSteps: updatePlan.steps,
|
|
10748
|
+
backupName: updatePlan.backupName,
|
|
10749
|
+
missingRequiredEnvKeys: missingEnvKeys,
|
|
10750
|
+
caddy: caddyPlan
|
|
10751
|
+
};
|
|
10752
|
+
if (flags.json === true) {
|
|
10753
|
+
printJson(preflight);
|
|
10754
|
+
return;
|
|
10755
|
+
}
|
|
10756
|
+
console.log("Server preflight plan:");
|
|
10757
|
+
console.log(` Role: ${role}`);
|
|
10758
|
+
console.log(` Profile: ${runtimePlan.profile ?? "n/a"}`);
|
|
10759
|
+
console.log(` Services: ${hasServiceFilter(flags) ? services.join(", ") : "all"}`);
|
|
10760
|
+
console.log(` Backup: ${updatePlan.backupName ?? "none"}`);
|
|
10761
|
+
console.log(` Env preflight: ${missingEnvKeys.length ? `missing ${missingEnvKeys.join(", ")}` : "ok"}`);
|
|
10762
|
+
console.log(` Health checks: ${runtimePlan.healthChecks.join(", ")}`);
|
|
10763
|
+
console.log(` Caddy steps: ${caddyPlan.steps.join(" -> ")}`);
|
|
10764
|
+
}
|
|
10765
|
+
async function serverCaddy(rest, flags) {
|
|
10766
|
+
const action = rest[0] ?? "status";
|
|
10767
|
+
if (!["check", "status", "diff", "apply"].includes(action)) {
|
|
10768
|
+
throw new Error("Usage: openmates server caddy check|status|diff|apply [--role core|upload|preview]");
|
|
10769
|
+
}
|
|
10770
|
+
const config = loadServerConfig();
|
|
10771
|
+
const role = getServerRole(flags, config);
|
|
10772
|
+
const appliedPath = typeof flags.config === "string" ? flags.config : "/etc/caddy/Caddyfile";
|
|
10773
|
+
const templatePath = packagedCaddyTemplatePath(role);
|
|
10774
|
+
if (!existsSync5(templatePath)) throw new Error(`Packaged Caddy template not found: ${templatePath}`);
|
|
10775
|
+
const plan = planCaddyCommand({ role, action, appliedPath });
|
|
10776
|
+
const payload = {
|
|
10777
|
+
...plan,
|
|
10778
|
+
templatePath,
|
|
10779
|
+
templateHash: fileHash(templatePath),
|
|
10780
|
+
appliedHash: fileHash(appliedPath),
|
|
10781
|
+
drift: fileHash(templatePath) !== fileHash(appliedPath)
|
|
10782
|
+
};
|
|
10783
|
+
if (flags.json === true) {
|
|
10784
|
+
printJson(payload);
|
|
10785
|
+
return;
|
|
10786
|
+
}
|
|
10787
|
+
if (action === "check") {
|
|
10788
|
+
execSync(`caddy validate --config ${shellQuote(templatePath)}`, { stdio: "inherit" });
|
|
10789
|
+
console.log(`Caddy template is valid for ${role}.`);
|
|
10790
|
+
return;
|
|
10791
|
+
}
|
|
10792
|
+
if (action === "diff") {
|
|
10793
|
+
if (!existsSync5(appliedPath)) throw new Error(`Applied Caddyfile not found: ${appliedPath}`);
|
|
10794
|
+
try {
|
|
10795
|
+
execSync(`diff -u ${shellQuote(appliedPath)} ${shellQuote(templatePath)}`, { stdio: "inherit" });
|
|
10796
|
+
} catch (error) {
|
|
10797
|
+
const status = typeof error === "object" && error !== null && "status" in error ? error.status : void 0;
|
|
10798
|
+
if (status !== 1) throw error;
|
|
10799
|
+
}
|
|
10800
|
+
return;
|
|
10801
|
+
}
|
|
10802
|
+
if (action === "apply") {
|
|
10803
|
+
execSync(`caddy validate --config ${shellQuote(templatePath)}`, { stdio: "inherit" });
|
|
10804
|
+
if (flags.yes !== true) {
|
|
10805
|
+
console.error(`
|
|
10806
|
+
WARNING: This will replace ${appliedPath} with the packaged ${role} Caddyfile.`);
|
|
10807
|
+
const confirmed = await confirmDestructive("APPLY CADDYFILE");
|
|
10808
|
+
if (!confirmed) {
|
|
10809
|
+
console.error("Caddy apply cancelled.");
|
|
10810
|
+
return;
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10813
|
+
const backupPath = `${appliedPath}.openmates-backup-${nowStamp()}`;
|
|
10814
|
+
try {
|
|
10815
|
+
if (existsSync5(appliedPath)) copyFileSync(appliedPath, backupPath);
|
|
10816
|
+
copyFileSync(templatePath, appliedPath);
|
|
10817
|
+
execSync("systemctl reload caddy", { stdio: "inherit" });
|
|
10818
|
+
} catch (error) {
|
|
10819
|
+
throw new Error(`Could not apply Caddyfile. Run with sudo or use --config <writable path>. ${error instanceof Error ? error.message : String(error)}`);
|
|
10820
|
+
}
|
|
10821
|
+
console.log(`Applied Caddyfile for ${role}. Backup: ${backupPath}`);
|
|
10822
|
+
return;
|
|
10823
|
+
}
|
|
10824
|
+
console.log(`Caddy ${action} plan:`);
|
|
10825
|
+
console.log(` Role: ${payload.role}`);
|
|
10826
|
+
console.log(` Template: ${payload.templatePath}`);
|
|
10827
|
+
console.log(` Applied: ${payload.appliedPath}`);
|
|
10828
|
+
console.log(` Template hash: ${payload.templateHash ?? "missing"}`);
|
|
10829
|
+
console.log(` Applied hash: ${payload.appliedHash ?? "missing"}`);
|
|
10830
|
+
console.log(` Drift: ${payload.drift ? "yes" : "no"}`);
|
|
10831
|
+
console.log(` Steps: ${payload.steps.join(" -> ")}`);
|
|
10832
|
+
}
|
|
9943
10833
|
function printServerHelp() {
|
|
9944
10834
|
console.log(`
|
|
9945
10835
|
OpenMates Server Management
|
|
@@ -9954,6 +10844,10 @@ Commands:
|
|
|
9954
10844
|
status Show server status (container health)
|
|
9955
10845
|
logs Display server logs
|
|
9956
10846
|
update Update to latest version (pull images, or git pull + rebuild for source installs)
|
|
10847
|
+
preflight Show role/update/Caddy preflight plan
|
|
10848
|
+
caddy Plan host-level Caddyfile check/status/diff/apply operations
|
|
10849
|
+
backup Create or list backups
|
|
10850
|
+
restore Restore a backup archive
|
|
9957
10851
|
make-admin Grant admin privileges to a user
|
|
9958
10852
|
ai Manage self-hosted local AI models
|
|
9959
10853
|
reset Reset server data (requires confirmation)
|
|
@@ -9962,33 +10856,57 @@ Commands:
|
|
|
9962
10856
|
Global Options:
|
|
9963
10857
|
--path <dir> Override the server installation directory
|
|
9964
10858
|
--json Output machine-readable JSON
|
|
10859
|
+
--role <role> Server role: core, upload, or preview (default: core)
|
|
9965
10860
|
--help Show this help message
|
|
9966
10861
|
|
|
9967
10862
|
Command Options:
|
|
9968
10863
|
install:
|
|
9969
10864
|
--path <dir> Install directory (default: ~/openmates)
|
|
9970
10865
|
--env-path <file> Copy a pre-existing .env file during install
|
|
10866
|
+
--profile <name> Core profile: minimal, standard, or production
|
|
10867
|
+
--with-alerts Include alertmanager in production profile planning
|
|
9971
10868
|
--image-tag <tag> Prebuilt image tag (default: CLI version tag)
|
|
9972
10869
|
--from-source Clone/build from source instead of using prebuilt GHCR images
|
|
9973
10870
|
--source-path <dir> Clone from a local checkout instead of GitHub (implies --from-source)
|
|
9974
10871
|
|
|
9975
10872
|
start:
|
|
9976
10873
|
--with-overrides Include admin UIs (Directus CMS, Grafana)
|
|
10874
|
+
--services <csv> Start only selected role services
|
|
10875
|
+
--exclude <csv> Start all role services except selected services
|
|
9977
10876
|
|
|
9978
10877
|
restart:
|
|
9979
10878
|
--rebuild Full rebuild (down + build + up) instead of graceful restart
|
|
10879
|
+
--services <csv> Restart only selected role services
|
|
10880
|
+
--exclude <csv> Restart all role services except selected services
|
|
9980
10881
|
|
|
9981
10882
|
logs:
|
|
9982
10883
|
--container <name> Filter logs to a specific service (e.g. api, cms)
|
|
10884
|
+
--services <csv> Filter logs to selected role services
|
|
10885
|
+
--exclude <csv> Filter logs to all role services except selected services
|
|
9983
10886
|
--follow, -f Stream logs in real time
|
|
9984
10887
|
--tail <n> Number of lines to show (default: 100)
|
|
9985
10888
|
|
|
9986
10889
|
update:
|
|
9987
10890
|
--dry-run Show update plan without changing files or containers
|
|
10891
|
+
--services <csv> Update only selected role services
|
|
10892
|
+
--exclude <csv> Update all role services except selected services
|
|
9988
10893
|
--image-tag <tag> Image mode: update to a specific prebuilt image tag
|
|
9989
10894
|
--channel <name> Image mode: update using stable/main or dev channel tags
|
|
10895
|
+
--continuous Run continuously in foreground, or use with install-service
|
|
10896
|
+
--interval <min> Foreground continuous update interval (default: 30)
|
|
10897
|
+
install-service --continuous --channel <name> --window <window>
|
|
9990
10898
|
--force Source mode: stash local changes before pulling
|
|
9991
10899
|
|
|
10900
|
+
backup:
|
|
10901
|
+
openmates server backup [--role core|upload|preview] [--output <file>] [--include-observability]
|
|
10902
|
+
openmates server backup list [--role core|upload|preview]
|
|
10903
|
+
|
|
10904
|
+
restore:
|
|
10905
|
+
openmates server restore --file <backup.tar.gz> [--role core|upload|preview] [--yes]
|
|
10906
|
+
|
|
10907
|
+
caddy:
|
|
10908
|
+
openmates server caddy check|status|diff|apply [--role core|upload|preview] [--config /etc/caddy/Caddyfile]
|
|
10909
|
+
|
|
9992
10910
|
reset:
|
|
9993
10911
|
--delete-user-data-only Only delete database and cache (preserve config)
|
|
9994
10912
|
--yes Skip confirmation prompt
|
|
@@ -10047,7 +10965,15 @@ async function handleServer(subcommand, rest, flags) {
|
|
|
10047
10965
|
case "install":
|
|
10048
10966
|
return serverInstall(flags);
|
|
10049
10967
|
case "update":
|
|
10050
|
-
return serverUpdate(flags);
|
|
10968
|
+
return serverUpdate(rest, flags);
|
|
10969
|
+
case "preflight":
|
|
10970
|
+
return serverPreflight(flags);
|
|
10971
|
+
case "caddy":
|
|
10972
|
+
return serverCaddy(rest, flags);
|
|
10973
|
+
case "backup":
|
|
10974
|
+
return serverBackup(rest, flags);
|
|
10975
|
+
case "restore":
|
|
10976
|
+
return serverRestore(flags);
|
|
10051
10977
|
case "reset":
|
|
10052
10978
|
return serverReset(flags);
|
|
10053
10979
|
case "make-admin":
|
|
@@ -26725,6 +27651,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
26725
27651
|
text: "Upload PDFs and read or search their content."
|
|
26726
27652
|
}
|
|
26727
27653
|
},
|
|
27654
|
+
mindmaps: {
|
|
27655
|
+
text: "Mind Maps",
|
|
27656
|
+
description: {
|
|
27657
|
+
text: "Create structured idea maps and brainstorm topic trees."
|
|
27658
|
+
}
|
|
27659
|
+
},
|
|
26728
27660
|
math: {
|
|
26729
27661
|
text: "Math",
|
|
26730
27662
|
description: {
|
|
@@ -26788,6 +27720,47 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
26788
27720
|
generated_by_cost: {
|
|
26789
27721
|
text: "Cost: {credits} credits"
|
|
26790
27722
|
},
|
|
27723
|
+
interests: {
|
|
27724
|
+
eyebrow: {
|
|
27725
|
+
text: "Private local personalization"
|
|
27726
|
+
},
|
|
27727
|
+
title: {
|
|
27728
|
+
text: "What are you interested in?"
|
|
27729
|
+
},
|
|
27730
|
+
continue: {
|
|
27731
|
+
text: "Continue"
|
|
27732
|
+
},
|
|
27733
|
+
software_development: {
|
|
27734
|
+
text: "software development"
|
|
27735
|
+
},
|
|
27736
|
+
use_the_cli: {
|
|
27737
|
+
text: "use the CLI"
|
|
27738
|
+
},
|
|
27739
|
+
open_source: {
|
|
27740
|
+
text: "open source"
|
|
27741
|
+
},
|
|
27742
|
+
read_developer_docs: {
|
|
27743
|
+
text: "read developer docs"
|
|
27744
|
+
},
|
|
27745
|
+
run_code: {
|
|
27746
|
+
text: "run code"
|
|
27747
|
+
},
|
|
27748
|
+
protect_my_privacy: {
|
|
27749
|
+
text: "protect my privacy"
|
|
27750
|
+
},
|
|
27751
|
+
summarize_documents: {
|
|
27752
|
+
text: "summarize documents"
|
|
27753
|
+
},
|
|
27754
|
+
find_apartments: {
|
|
27755
|
+
text: "find apartments"
|
|
27756
|
+
},
|
|
27757
|
+
local_life: {
|
|
27758
|
+
text: "local life"
|
|
27759
|
+
},
|
|
27760
|
+
learn_anything: {
|
|
27761
|
+
text: "learn anything"
|
|
27762
|
+
}
|
|
27763
|
+
},
|
|
26791
27764
|
new_chat: {
|
|
26792
27765
|
text: "New chat"
|
|
26793
27766
|
},
|
|
@@ -27213,6 +28186,9 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
27213
28186
|
learn_coding: {
|
|
27214
28187
|
text: "How do I start learning to code?"
|
|
27215
28188
|
},
|
|
28189
|
+
use_openmates_cli_api: {
|
|
28190
|
+
text: "Show me how to use OpenMates from the CLI or API"
|
|
28191
|
+
},
|
|
27216
28192
|
stock_market: {
|
|
27217
28193
|
text: "Explain how the stock market works"
|
|
27218
28194
|
},
|
|
@@ -30378,6 +31354,32 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
|
|
|
30378
31354
|
text: "Plot"
|
|
30379
31355
|
}
|
|
30380
31356
|
},
|
|
31357
|
+
mindmaps: {
|
|
31358
|
+
mindmap: {
|
|
31359
|
+
text: "Mind Map"
|
|
31360
|
+
},
|
|
31361
|
+
counts: {
|
|
31362
|
+
text: "{nodes} nodes \xB7 {edges} edges"
|
|
31363
|
+
},
|
|
31364
|
+
invalid_json: {
|
|
31365
|
+
text: "Invalid mind map JSON"
|
|
31366
|
+
},
|
|
31367
|
+
invalid_content: {
|
|
31368
|
+
text: "Invalid content"
|
|
31369
|
+
},
|
|
31370
|
+
validation_warnings: {
|
|
31371
|
+
text: "Validation warnings"
|
|
31372
|
+
},
|
|
31373
|
+
source: {
|
|
31374
|
+
text: "Source"
|
|
31375
|
+
},
|
|
31376
|
+
expand: {
|
|
31377
|
+
text: "Expand {label}"
|
|
31378
|
+
},
|
|
31379
|
+
collapse: {
|
|
31380
|
+
text: "Collapse {label}"
|
|
31381
|
+
}
|
|
31382
|
+
},
|
|
30381
31383
|
diagrams: {
|
|
30382
31384
|
mermaid: {
|
|
30383
31385
|
text: "Mermaid Diagram"
|
|
@@ -36409,6 +37411,21 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
|
|
|
36409
37411
|
},
|
|
36410
37412
|
import_importing: {
|
|
36411
37413
|
text: "Importing\u2026"
|
|
37414
|
+
},
|
|
37415
|
+
interests: {
|
|
37416
|
+
text: "Interests"
|
|
37417
|
+
},
|
|
37418
|
+
interests_description: {
|
|
37419
|
+
text: "Choose the topics OpenMates should prioritize when it suggests chats, examples, and product tips."
|
|
37420
|
+
},
|
|
37421
|
+
interests_privacy_note: {
|
|
37422
|
+
text: "These preferences are encrypted on your device before syncing. The server stores only ciphertext."
|
|
37423
|
+
},
|
|
37424
|
+
interests_saved: {
|
|
37425
|
+
text: "Interests saved"
|
|
37426
|
+
},
|
|
37427
|
+
interests_save_error: {
|
|
37428
|
+
text: "Could not save interests. Please try again."
|
|
36412
37429
|
}
|
|
36413
37430
|
},
|
|
36414
37431
|
ai: {
|
|
@@ -39929,6 +40946,9 @@ As of mid-2026, the severe supply shocks from the 2024\u20132025 avian flu have
|
|
|
39929
40946
|
server_will_be_restarted: {
|
|
39930
40947
|
text: "The server will be restarted and\ntherefore briefly offline once\nthe update has been installed."
|
|
39931
40948
|
},
|
|
40949
|
+
cli_managed_update_notice: {
|
|
40950
|
+
text: "Install updates from the server host with <code>openmates server update</code>."
|
|
40951
|
+
},
|
|
39932
40952
|
installing_update: {
|
|
39933
40953
|
text: "Installing update..."
|
|
39934
40954
|
},
|
|
@@ -42301,7 +43321,7 @@ function buildAssistantFeedbackDecision(rating) {
|
|
|
42301
43321
|
|
|
42302
43322
|
// src/benchmark.ts
|
|
42303
43323
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
42304
|
-
import { existsSync as existsSync6, mkdtempSync, readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync4 } from "fs";
|
|
43324
|
+
import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
42305
43325
|
import { tmpdir } from "os";
|
|
42306
43326
|
import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
|
|
42307
43327
|
import { fileURLToPath } from "url";
|
|
@@ -43100,7 +44120,7 @@ function loadProviderPricing() {
|
|
|
43100
44120
|
const providersDir = findProvidersDir();
|
|
43101
44121
|
const pricing = /* @__PURE__ */ new Map();
|
|
43102
44122
|
if (!providersDir) return pricing;
|
|
43103
|
-
for (const fileName of
|
|
44123
|
+
for (const fileName of readdirSync2(providersDir)) {
|
|
43104
44124
|
if (!fileName.endsWith(".yml")) continue;
|
|
43105
44125
|
const filePath = join4(providersDir, fileName);
|
|
43106
44126
|
const text = readFileSync6(filePath, "utf-8");
|
|
@@ -43285,7 +44305,7 @@ function defaultImageFixturePath() {
|
|
|
43285
44305
|
const fixtureDir = join4(dirname2(fileURLToPath(import.meta.url)), "..", "fixtures");
|
|
43286
44306
|
const fixturePath = join4(fixtureDir, "brandenburger-tor.png");
|
|
43287
44307
|
if (existsSync6(fixturePath)) return fixturePath;
|
|
43288
|
-
const tempDir =
|
|
44308
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "openmates-benchmark-"));
|
|
43289
44309
|
const tempPath = join4(tempDir, "brandenburger-tor.svg");
|
|
43290
44310
|
writeFileSync4(tempPath, FIXTURE_IMAGE_SVG, "utf-8");
|
|
43291
44311
|
return tempPath;
|
|
@@ -44804,6 +45824,9 @@ async function handleEmbeds(client, subcommand, rest, flags) {
|
|
|
44804
45824
|
var SETTINGS_EXECUTABLE_COMMANDS = [
|
|
44805
45825
|
{ path: ["account", "info"], description: "Show account info", examples: ["openmates settings account info --json"] },
|
|
44806
45826
|
{ path: ["account", "timezone", "set"], description: "Set account timezone", examples: ["openmates settings account timezone set Europe/Berlin"] },
|
|
45827
|
+
{ path: ["account", "interests", "list"], description: "Show encrypted account topic interests", examples: ["openmates settings account interests list --json"] },
|
|
45828
|
+
{ path: ["account", "interests", "set"], description: "Set encrypted account topic interests", examples: ["openmates settings account interests set software_development use_the_cli"] },
|
|
45829
|
+
{ path: ["account", "interests", "clear"], description: "Clear encrypted account topic interests", examples: ["openmates settings account interests clear --yes"] },
|
|
44807
45830
|
{ path: ["account", "export", "manifest"], description: "Show account export manifest", examples: ["openmates settings account export manifest --json"] },
|
|
44808
45831
|
{ path: ["account", "export", "data"], description: "Fetch account export data", examples: ["openmates settings account export data --json"] },
|
|
44809
45832
|
{ 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 +45928,30 @@ async function printSettingsMutationResult(resultPromise, flags) {
|
|
|
44905
45928
|
process.stdout.write("\x1B[32m\u2713\x1B[0m Settings updated\n");
|
|
44906
45929
|
if (result && typeof result === "object") printGenericObject(result);
|
|
44907
45930
|
}
|
|
45931
|
+
function printTopicPreferences(preferences, flags, successLabel) {
|
|
45932
|
+
const payload = preferences ?? {
|
|
45933
|
+
version: 1,
|
|
45934
|
+
selectedTagIds: [],
|
|
45935
|
+
updatedAt: null
|
|
45936
|
+
};
|
|
45937
|
+
const result = {
|
|
45938
|
+
...payload,
|
|
45939
|
+
availableTagIds: INTEREST_TAG_IDS
|
|
45940
|
+
};
|
|
45941
|
+
if (flags.json === true) {
|
|
45942
|
+
printJson2(result);
|
|
45943
|
+
return;
|
|
45944
|
+
}
|
|
45945
|
+
if (successLabel) {
|
|
45946
|
+
process.stdout.write(`\x1B[32m\u2713\x1B[0m ${successLabel}
|
|
45947
|
+
`);
|
|
45948
|
+
}
|
|
45949
|
+
const selected = result.selectedTagIds.length > 0 ? result.selectedTagIds.join(", ") : "none";
|
|
45950
|
+
process.stdout.write(`Selected interests: ${selected}
|
|
45951
|
+
`);
|
|
45952
|
+
process.stdout.write(`Available interests: ${INTEREST_TAG_IDS.join(", ")}
|
|
45953
|
+
`);
|
|
45954
|
+
}
|
|
44908
45955
|
function printReportIssueCreateResult(result, flags) {
|
|
44909
45956
|
if (flags.json === true) {
|
|
44910
45957
|
printJson2(result);
|
|
@@ -45428,6 +46475,30 @@ async function handleSettings(client, subcommand, rest, flags) {
|
|
|
45428
46475
|
);
|
|
45429
46476
|
return;
|
|
45430
46477
|
}
|
|
46478
|
+
if (matches(tokens, ["account", "interests", "list"])) {
|
|
46479
|
+
const preferences = await client.getTopicPreferences();
|
|
46480
|
+
printTopicPreferences(preferences, flags);
|
|
46481
|
+
return;
|
|
46482
|
+
}
|
|
46483
|
+
if (matches(tokens, ["account", "interests", "set"])) {
|
|
46484
|
+
const selectedTagIds = rest.slice(2);
|
|
46485
|
+
if (selectedTagIds.length === 0) {
|
|
46486
|
+
throw new Error(
|
|
46487
|
+
`Missing interest tag IDs. Use one or more of: ${INTEREST_TAG_IDS.join(", ")}`
|
|
46488
|
+
);
|
|
46489
|
+
}
|
|
46490
|
+
const preferences = await client.setTopicPreferences(selectedTagIds);
|
|
46491
|
+
printTopicPreferences(preferences, flags, "Interests updated");
|
|
46492
|
+
return;
|
|
46493
|
+
}
|
|
46494
|
+
if (matches(tokens, ["account", "interests", "clear"])) {
|
|
46495
|
+
if (flags.yes !== true) {
|
|
46496
|
+
await confirmOrExit("Clear account interests? [y/N] ");
|
|
46497
|
+
}
|
|
46498
|
+
const preferences = await client.clearTopicPreferences();
|
|
46499
|
+
printTopicPreferences(preferences, flags, "Interests cleared");
|
|
46500
|
+
return;
|
|
46501
|
+
}
|
|
45431
46502
|
if (matches(tokens, ["account", "export", "manifest"])) {
|
|
45432
46503
|
await printSettingsResult(client.settingsGet("export-account-manifest"), flags);
|
|
45433
46504
|
return;
|
|
@@ -48323,6 +49394,8 @@ if (isCliEntrypoint()) {
|
|
|
48323
49394
|
}
|
|
48324
49395
|
|
|
48325
49396
|
export {
|
|
49397
|
+
INTEREST_TAG_IDS,
|
|
49398
|
+
normalizeInterestTagIds,
|
|
48326
49399
|
MEMORY_TYPE_REGISTRY,
|
|
48327
49400
|
MATE_NAMES,
|
|
48328
49401
|
deriveAppUrl,
|