@cybermem/cli 0.9.12 ā 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/install.js +430 -0
- package/dist/commands/reset.js +18 -2
- package/dist/commands/uninstall.js +145 -0
- package/dist/commands/upgrade.js +91 -52
- package/dist/index.js +18 -5
- package/dist/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
- package/dist/templates/ansible/playbooks/reset-db.yml +44 -0
- package/dist/templates/auth-sidecar/Dockerfile +2 -10
- package/dist/templates/auth-sidecar/package.json +1 -3
- package/dist/templates/auth-sidecar/server.js +149 -110
- package/dist/templates/charts/cybermem/.helmignore +13 -0
- package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
- package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
- package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
- package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
- package/dist/templates/charts/cybermem/templates/secret.yaml +9 -0
- package/dist/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
- package/dist/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
- package/dist/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
- package/dist/templates/charts/cybermem/values-vps.yaml +8 -4
- package/dist/templates/charts/cybermem/values.yaml +17 -9
- package/dist/templates/docker-compose.yml +103 -78
- package/dist/templates/monitoring/log_exporter/exporter.py +22 -29
- package/dist/templates/monitoring/traefik/traefik.yml +1 -4
- package/package.json +9 -3
- package/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
- package/templates/ansible/playbooks/reset-db.yml +44 -0
- package/templates/auth-sidecar/Dockerfile +2 -10
- package/templates/auth-sidecar/package.json +1 -3
- package/templates/auth-sidecar/server.js +149 -110
- package/templates/charts/cybermem/.helmignore +13 -0
- package/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
- package/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
- package/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
- package/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
- package/templates/charts/cybermem/templates/secret.yaml +9 -0
- package/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
- package/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
- package/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
- package/templates/charts/cybermem/values-vps.yaml +8 -4
- package/templates/charts/cybermem/values.yaml +17 -9
- package/templates/docker-compose.yml +103 -78
- package/templates/monitoring/log_exporter/exporter.py +22 -29
- package/templates/monitoring/traefik/traefik.yml +1 -4
- package/dist/commands/__tests__/backup.test.js +0 -75
- package/dist/commands/__tests__/restore.test.js +0 -70
- package/dist/commands/deploy.js +0 -239
- package/dist/commands/init.js +0 -362
- package/dist/commands/login.js +0 -165
- package/dist/templates/envs/local.example +0 -27
- package/dist/templates/envs/rpi.example +0 -27
- package/dist/templates/envs/vps.example +0 -25
- package/dist/templates/monitoring/instructions_injector/Dockerfile +0 -15
- package/dist/templates/monitoring/instructions_injector/injector.py +0 -137
- package/dist/templates/monitoring/instructions_injector/requirements.txt +0 -3
- package/dist/templates/openmemory/Dockerfile +0 -19
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.install = install;
|
|
40
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
41
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
42
|
+
const execa_1 = __importDefault(require("execa"));
|
|
43
|
+
const fs_1 = __importDefault(require("fs"));
|
|
44
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
45
|
+
const os_1 = __importDefault(require("os"));
|
|
46
|
+
const path_1 = __importDefault(require("path"));
|
|
47
|
+
// Helper to handle and suggest fixes for common errors
|
|
48
|
+
function handleExecError(error, context) {
|
|
49
|
+
const stderr = error.stderr || "";
|
|
50
|
+
let suggestion = "";
|
|
51
|
+
if (stderr.includes("Permission denied") || stderr.includes("publickey")) {
|
|
52
|
+
suggestion =
|
|
53
|
+
"\nš” Next Step: Ensure your SSH keys are added to the remote host: `ssh-copy-id user@host`";
|
|
54
|
+
}
|
|
55
|
+
else if (stderr.includes("Connection timed out") ||
|
|
56
|
+
stderr.includes("Could not resolve host")) {
|
|
57
|
+
suggestion =
|
|
58
|
+
"\nš” Next Step: Check your network connection or the remote host's availability.";
|
|
59
|
+
}
|
|
60
|
+
else if (stderr.includes("ansible-playbook: command not found")) {
|
|
61
|
+
suggestion =
|
|
62
|
+
"\nš” Next Step: Install Ansible: `brew install ansible` (on macOS).";
|
|
63
|
+
}
|
|
64
|
+
else if (stderr.includes("docker-compose: command not found")) {
|
|
65
|
+
suggestion =
|
|
66
|
+
"\nš” Next Step: Install Docker and ensure docker-compose is in your PATH.";
|
|
67
|
+
}
|
|
68
|
+
else if (stderr.includes("port is already allocated")) {
|
|
69
|
+
suggestion =
|
|
70
|
+
"\nš” Next Step: Check for port conflicts (8626, 3000, 8080) and stop competing services.";
|
|
71
|
+
}
|
|
72
|
+
console.error(chalk_1.default.red(`\nā ${context} failed:`), error.message);
|
|
73
|
+
if (suggestion)
|
|
74
|
+
console.log(chalk_1.default.yellow(suggestion));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
// Hash token using PBKDF2 (built-in, no bcrypt dependency)
|
|
78
|
+
async function hashToken(token) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
// Use a fixed salt prefix for deterministic validation
|
|
81
|
+
const salt = crypto_1.default
|
|
82
|
+
.createHash("sha256")
|
|
83
|
+
.update("cybermem-salt-v1")
|
|
84
|
+
.digest("hex")
|
|
85
|
+
.slice(0, 16);
|
|
86
|
+
crypto_1.default.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
|
|
87
|
+
if (err)
|
|
88
|
+
reject(err);
|
|
89
|
+
else
|
|
90
|
+
resolve(key.toString("hex"));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function install(options) {
|
|
95
|
+
// Determine target from flags
|
|
96
|
+
let target = "local";
|
|
97
|
+
if (options.rpi)
|
|
98
|
+
target = "rpi";
|
|
99
|
+
if (options.vps)
|
|
100
|
+
target = "vps";
|
|
101
|
+
const isStaging = !!options.staging;
|
|
102
|
+
const envType = isStaging ? "staging" : "prod";
|
|
103
|
+
const useTailscale = !!options.remoteAccess;
|
|
104
|
+
const networkType = target === "local" ? "" : useTailscale ? "ts" : "lan";
|
|
105
|
+
const envLabel = [target === "local" ? "localhost" : target, networkType, envType].filter(Boolean).join("-");
|
|
106
|
+
console.log(chalk_1.default.blue(`Initializing CyberMem (${envLabel})...`));
|
|
107
|
+
try {
|
|
108
|
+
// Resolve Template Directory (Support both Dev and Prod)
|
|
109
|
+
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
110
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
111
|
+
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
112
|
+
}
|
|
113
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
114
|
+
templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
|
|
115
|
+
}
|
|
116
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
117
|
+
// Fallback for different build structures
|
|
118
|
+
templateDir = path_1.default.resolve(__dirname, "../templates");
|
|
119
|
+
}
|
|
120
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
121
|
+
throw new Error(`Templates not found at ${templateDir}. Please ensure package is built correctly.`);
|
|
122
|
+
}
|
|
123
|
+
// Generate secure token for ALL deployment types
|
|
124
|
+
// Format: sk- + 32 chars (16 bytes hex)
|
|
125
|
+
const accessToken = `sk-${crypto_1.default.randomBytes(16).toString("hex")}`;
|
|
126
|
+
const tokenHash = await hashToken(accessToken); // PBKDF2 hash
|
|
127
|
+
const tokenId = crypto_1.default.randomBytes(8).toString("hex");
|
|
128
|
+
const tokenName = isStaging ? "staging-verifier" : "admin-cli";
|
|
129
|
+
if (target === "local") {
|
|
130
|
+
const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
|
|
131
|
+
if (!fs_1.default.existsSync(composeFile)) {
|
|
132
|
+
console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
// Home Directory Config
|
|
136
|
+
const homeDir = os_1.default.homedir();
|
|
137
|
+
const configDir = path_1.default.join(homeDir, ".cybermem");
|
|
138
|
+
const envFile = path_1.default.join(configDir, ".env");
|
|
139
|
+
const dataDir = path_1.default.join(configDir, isStaging ? "data-staging" : "data");
|
|
140
|
+
// 1. Ensure ~/.cybermem exists
|
|
141
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
142
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
if (!fs_1.default.existsSync(dataDir)) {
|
|
145
|
+
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
// 2. Local Mode
|
|
148
|
+
if (!fs_1.default.existsSync(envFile)) {
|
|
149
|
+
console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
|
|
150
|
+
const templateEnv = path_1.default.join(templateDir, "envs/local.env");
|
|
151
|
+
const envContent = fs_1.default.readFileSync(templateEnv, "utf-8");
|
|
152
|
+
fs_1.default.writeFileSync(envFile, envContent);
|
|
153
|
+
console.log(chalk_1.default.green(`Created .env at ${envFile}`));
|
|
154
|
+
}
|
|
155
|
+
const dbPath = path_1.default.join(dataDir, "openmemory.sqlite");
|
|
156
|
+
const secretsDir = path_1.default.join(configDir, "secrets");
|
|
157
|
+
const secretPath = path_1.default.join(secretsDir, "om_api_key");
|
|
158
|
+
let localAccessToken = accessToken;
|
|
159
|
+
// 1.5 Load existing local secret if present (SSoT)
|
|
160
|
+
if (fs_1.default.existsSync(secretPath)) {
|
|
161
|
+
localAccessToken = fs_1.default.readFileSync(secretPath, "utf-8").trim();
|
|
162
|
+
console.log(chalk_1.default.gray(`Loaded existing SSoT token from ${secretPath}`));
|
|
163
|
+
}
|
|
164
|
+
// Initialize access token and store hash in SQLite (BEFORE starting containers to avoid race/lock)
|
|
165
|
+
console.log(chalk_1.default.blue("Initializing local access token..."));
|
|
166
|
+
// Check if token key already exists (idempotency)
|
|
167
|
+
try {
|
|
168
|
+
const sqlite3 = await Promise.resolve().then(() => __importStar(require("sqlite3")));
|
|
169
|
+
const db = new sqlite3.default.Database(dbPath);
|
|
170
|
+
// Promisify db.run and db.get for cleaner logic
|
|
171
|
+
const run = (sql, params = []) => new Promise((resolve, reject) => {
|
|
172
|
+
db.run(sql, params, (err) => (err ? reject(err) : resolve()));
|
|
173
|
+
});
|
|
174
|
+
const get = (sql, params = []) => new Promise((resolve, reject) => {
|
|
175
|
+
db.get(sql, params, (err, row) => err ? reject(err) : resolve(row));
|
|
176
|
+
});
|
|
177
|
+
// Check if table exists and has correct schema
|
|
178
|
+
const tableInfo = await new Promise((resolve, reject) => {
|
|
179
|
+
db.all("PRAGMA table_info(access_keys)", (err, rows) => {
|
|
180
|
+
if (err)
|
|
181
|
+
reject(err);
|
|
182
|
+
else
|
|
183
|
+
resolve(rows);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
const hasIdColumn = tableInfo.some((col) => col.name === "id");
|
|
187
|
+
if (tableInfo.length > 0 && !hasIdColumn) {
|
|
188
|
+
console.warn(chalk_1.default.yellow("Detected malformed access_keys table. Recreating..."));
|
|
189
|
+
await run("DROP TABLE access_keys");
|
|
190
|
+
}
|
|
191
|
+
// Create table if not exists with hash matching current localAccessToken
|
|
192
|
+
const currentHash = await hashToken(localAccessToken);
|
|
193
|
+
await run(`CREATE TABLE IF NOT EXISTS access_keys (
|
|
194
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
195
|
+
key_hash TEXT NOT NULL,
|
|
196
|
+
name TEXT DEFAULT 'default',
|
|
197
|
+
user_id TEXT DEFAULT 'default',
|
|
198
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
199
|
+
last_used_at TEXT,
|
|
200
|
+
is_active INTEGER DEFAULT 1
|
|
201
|
+
);`);
|
|
202
|
+
const row = await get("SELECT COUNT(*) as count FROM access_keys WHERE is_active = 1");
|
|
203
|
+
if (row && row.count > 0) {
|
|
204
|
+
db.close();
|
|
205
|
+
console.log(chalk_1.default.gray("Access token configuration preserved."));
|
|
206
|
+
// proceed without showing token
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// Insert new key
|
|
210
|
+
await run("INSERT INTO access_keys (id, key_hash, name, user_id) VALUES (?, ?, ?, ?)", [tokenId, currentHash, tokenName, "admin"]);
|
|
211
|
+
db.close();
|
|
212
|
+
// Store token as Docker Secret (File)
|
|
213
|
+
if (!fs_1.default.existsSync(secretsDir))
|
|
214
|
+
fs_1.default.mkdirSync(secretsDir, { recursive: true });
|
|
215
|
+
fs_1.default.writeFileSync(secretPath, localAccessToken, {
|
|
216
|
+
encoding: "utf-8",
|
|
217
|
+
mode: 0o600,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
console.warn(chalk_1.default.yellow("Could not initialize access token: " + e.message));
|
|
223
|
+
}
|
|
224
|
+
console.log(chalk_1.default.blue("Starting CyberMem services in Local Mode..."));
|
|
225
|
+
try {
|
|
226
|
+
// Detect if we should use 'docker compose' (v2) or 'docker-compose' (v1)
|
|
227
|
+
let composeCmd = ["docker-compose"];
|
|
228
|
+
try {
|
|
229
|
+
await (0, execa_1.default)("docker", ["compose", "version"]);
|
|
230
|
+
composeCmd = ["docker", "compose"];
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
// Fallback to v1 if v2 not found
|
|
234
|
+
}
|
|
235
|
+
await (0, execa_1.default)(composeCmd[0], [
|
|
236
|
+
...composeCmd.slice(1),
|
|
237
|
+
"-f",
|
|
238
|
+
composeFile,
|
|
239
|
+
"--env-file",
|
|
240
|
+
envFile,
|
|
241
|
+
"--project-name",
|
|
242
|
+
"cybermem" + (isStaging ? "-staging" : ""),
|
|
243
|
+
"up",
|
|
244
|
+
"-d",
|
|
245
|
+
"--build",
|
|
246
|
+
"--remove-orphans",
|
|
247
|
+
], {
|
|
248
|
+
stdio: "inherit",
|
|
249
|
+
env: {
|
|
250
|
+
...process.env,
|
|
251
|
+
DATA_DIR: dataDir,
|
|
252
|
+
SECRETS_DIR: path_1.default.join(configDir, "secrets"), // Pass secrets dir context
|
|
253
|
+
CYBERMEM_ENV_PATH: envFile,
|
|
254
|
+
OM_API_KEY: "", // Legacy env var disabled
|
|
255
|
+
PROJECT_NAME: "cybermem" + (isStaging ? "-staging" : ""),
|
|
256
|
+
// Refined environment tagging
|
|
257
|
+
CYBERMEM_ENV: envType,
|
|
258
|
+
CYBERMEM_INSTANCE: target,
|
|
259
|
+
CYBERMEM_TAILSCALE: useTailscale ? "true" : "false",
|
|
260
|
+
// Port parameterization
|
|
261
|
+
TRAEFIK_PORT: isStaging ? "8625" : "8626",
|
|
262
|
+
DASHBOARD_PORT: isStaging ? "3001" : "3000",
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
handleExecError(e, "Local deployment");
|
|
268
|
+
}
|
|
269
|
+
await printSuccessMessage(true, localAccessToken);
|
|
270
|
+
async function printSuccessMessage(showToken, token) {
|
|
271
|
+
console.log(chalk_1.default.green("\nš CyberMem Installed!"));
|
|
272
|
+
console.log("");
|
|
273
|
+
if (showToken && token) {
|
|
274
|
+
console.log(chalk_1.default.bold("ā” Your Access Token (save this!):"));
|
|
275
|
+
console.log(chalk_1.default.cyan.bold(` ${token}`));
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(chalk_1.default.gray(" Use this token to connect MCP clients from other devices."));
|
|
278
|
+
console.log(chalk_1.default.gray(" You can regenerate it from Dashboard Settings."));
|
|
279
|
+
console.log("");
|
|
280
|
+
}
|
|
281
|
+
const entryPort = isStaging ? "8625" : "8626";
|
|
282
|
+
console.log(chalk_1.default.bold("Next Steps:"));
|
|
283
|
+
console.log(` 1. Open ${chalk_1.default.underline(`http://localhost:${entryPort}/client-setup`)} to connect your MCP clients`);
|
|
284
|
+
console.log(` 2. Local access is auto-authenticated (no token needed on localhost)`);
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log(chalk_1.default.dim("Local mode is active: No auth required for connections from this device."));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (target === "rpi" || target === "vps") {
|
|
290
|
+
const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
|
|
291
|
+
// Use --host flag or prompt interactively
|
|
292
|
+
let sshHost;
|
|
293
|
+
if (options.host) {
|
|
294
|
+
sshHost = options.host;
|
|
295
|
+
if (!sshHost.includes("@")) {
|
|
296
|
+
throw new Error("--host format must be user@host (e.g. pi@raspberrypi.local)");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const answers = await inquirer_1.default.prompt([
|
|
301
|
+
{
|
|
302
|
+
type: "input",
|
|
303
|
+
name: "host",
|
|
304
|
+
message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
|
|
305
|
+
validate: (input) => input.includes("@") ? true : "Format must be user@host",
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
sshHost = answers.host;
|
|
309
|
+
}
|
|
310
|
+
console.log(chalk_1.default.blue(`Remote deploying to ${sshHost} via Ansible...`));
|
|
311
|
+
// 1. Check if ansible-playbook is available
|
|
312
|
+
try {
|
|
313
|
+
await (0, execa_1.default)("ansible-playbook", ["--version"]);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
throw new Error("ansible-playbook not found. Please install Ansible on your MacBook to use remote deployment.");
|
|
317
|
+
}
|
|
318
|
+
// 2. Parse sshHost (user@host)
|
|
319
|
+
const [sshUser, host] = sshHost.split("@");
|
|
320
|
+
if (!host) {
|
|
321
|
+
throw new Error("Invalid SSH Host format. Use user@host");
|
|
322
|
+
}
|
|
323
|
+
// 3. Build images locally from source
|
|
324
|
+
console.log(chalk_1.default.blue("Building Docker images locally..."));
|
|
325
|
+
let composeCmd = ["docker-compose"];
|
|
326
|
+
try {
|
|
327
|
+
await (0, execa_1.default)("docker", ["compose", "version"]);
|
|
328
|
+
composeCmd = ["docker", "compose"];
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
// Fallback to v1 if v2 not found
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await (0, execa_1.default)(composeCmd[0], [
|
|
335
|
+
...composeCmd.slice(1),
|
|
336
|
+
"-f",
|
|
337
|
+
composeFile,
|
|
338
|
+
"build",
|
|
339
|
+
], {
|
|
340
|
+
stdio: "inherit",
|
|
341
|
+
env: {
|
|
342
|
+
...process.env,
|
|
343
|
+
CYBERMEM_ENV: envType,
|
|
344
|
+
PROJECT_NAME: isStaging ? "cybermem-staging" : "cybermem",
|
|
345
|
+
TRAEFIK_PORT: isStaging ? "8625" : "8626",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
handleExecError(e, "Local image build");
|
|
351
|
+
}
|
|
352
|
+
// 4. Transfer built images to remote host
|
|
353
|
+
console.log(chalk_1.default.blue(`Transferring images to ${host}...`));
|
|
354
|
+
// Get list of built images from compose file (filter cybermem-* only)
|
|
355
|
+
try {
|
|
356
|
+
const { stdout: allImages } = await (0, execa_1.default)(composeCmd[0], [...composeCmd.slice(1), "-f", composeFile, "config", "--images"]);
|
|
357
|
+
const builtImages = allImages
|
|
358
|
+
.trim()
|
|
359
|
+
.split("\n")
|
|
360
|
+
.filter((img) => img.includes("cybermem-"));
|
|
361
|
+
if (builtImages.length === 0) {
|
|
362
|
+
throw new Error("No cybermem images found after build");
|
|
363
|
+
}
|
|
364
|
+
console.log(chalk_1.default.gray(` Transferring ${builtImages.length} images...`));
|
|
365
|
+
await (0, execa_1.default)("bash", [
|
|
366
|
+
"-c",
|
|
367
|
+
`docker save ${builtImages.join(" ")} | ssh -o StrictHostKeyChecking=no ${sshHost} docker load`,
|
|
368
|
+
], { stdio: "inherit" });
|
|
369
|
+
console.log(chalk_1.default.green(" ā
Images transferred"));
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
handleExecError(e, "Image transfer");
|
|
373
|
+
}
|
|
374
|
+
// 5. Resolve Ansible Paths
|
|
375
|
+
const playbookPath = path_1.default.join(templateDir, "ansible/playbooks/deploy-cybermem.yml");
|
|
376
|
+
const ansibleDir = path_1.default.join(templateDir, "ansible");
|
|
377
|
+
if (!fs_1.default.existsSync(playbookPath)) {
|
|
378
|
+
throw new Error(`Ansible playbook not found at ${playbookPath}`);
|
|
379
|
+
}
|
|
380
|
+
// 6. Run Ansible Playbook (skip pull ā images already loaded)
|
|
381
|
+
console.log(chalk_1.default.blue("Deploying via Ansible..."));
|
|
382
|
+
const inventory = `${host},`;
|
|
383
|
+
try {
|
|
384
|
+
await (0, execa_1.default)("ansible-playbook", [
|
|
385
|
+
"-i",
|
|
386
|
+
inventory,
|
|
387
|
+
"-u",
|
|
388
|
+
sshUser,
|
|
389
|
+
playbookPath,
|
|
390
|
+
"--extra-vars",
|
|
391
|
+
`ansible_ssh_extra_args='-o StrictHostKeyChecking=no'`,
|
|
392
|
+
"--extra-vars",
|
|
393
|
+
`skip_pull=true`,
|
|
394
|
+
"--extra-vars",
|
|
395
|
+
`auth_token_hash=${tokenHash}`,
|
|
396
|
+
"--extra-vars",
|
|
397
|
+
`auth_token_id=${tokenId}`,
|
|
398
|
+
"--extra-vars",
|
|
399
|
+
`auth_token_name=${tokenName}`,
|
|
400
|
+
"--extra-vars",
|
|
401
|
+
`auth_token_value=${accessToken}`,
|
|
402
|
+
"--extra-vars",
|
|
403
|
+
`cybermem_env=${envType}`,
|
|
404
|
+
"--extra-vars",
|
|
405
|
+
`TRAEFIK_PORT=${isStaging ? "8625" : "8626"}`,
|
|
406
|
+
"--extra-vars",
|
|
407
|
+
`CYBERMEM_TAILSCALE=${useTailscale}`,
|
|
408
|
+
"--extra-vars",
|
|
409
|
+
`PROJECT_NAME=${isStaging ? "cybermem-staging" : "cybermem"}`,
|
|
410
|
+
], {
|
|
411
|
+
stdio: "inherit",
|
|
412
|
+
cwd: ansibleDir,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
handleExecError(e, "Remote deployment");
|
|
417
|
+
}
|
|
418
|
+
const entryPort = isStaging ? "8625" : "8626";
|
|
419
|
+
console.log(chalk_1.default.green("\nā
Remote deployment successful via Ansible!"));
|
|
420
|
+
console.log(chalk_1.default.bold("ā” Your Initial Access Token:"));
|
|
421
|
+
console.log(chalk_1.default.cyan.bold(` ${accessToken}`));
|
|
422
|
+
console.log("");
|
|
423
|
+
console.log(chalk_1.default.bold(`Dashboard: http://${host}:${entryPort}`));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
console.error(chalk_1.default.red("Deployment failed:"), error);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
}
|
package/dist/commands/reset.js
CHANGED
|
@@ -7,7 +7,8 @@ exports.reset = reset;
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const ora_1 = __importDefault(require("ora"));
|
|
10
|
-
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
async function reset(options) {
|
|
11
12
|
// ā ļø PRODUCTION PROTECTION - Never wipe RPi database
|
|
12
13
|
const cyberMemEnv = process.env.CYBERMEM_ENV || "";
|
|
13
14
|
const hostname = process.env.HOSTNAME || "";
|
|
@@ -17,9 +18,24 @@ async function reset() {
|
|
|
17
18
|
console.error(chalk_1.default.gray(" Use npx @cybermem/cli backup first, then manually clear if needed."));
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
21
|
+
// Confirmation Prompt
|
|
22
|
+
if (!options.force) {
|
|
23
|
+
const { confirm } = await inquirer_1.default.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: "confirm",
|
|
26
|
+
name: "confirm",
|
|
27
|
+
message: "ā ļø WARNING: This will corrupt all memories. Are you sure?",
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
if (!confirm) {
|
|
32
|
+
console.log(chalk_1.default.gray("Reset cancelled."));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
const spinner = (0, ora_1.default)("Resetting CyberMem database...").start();
|
|
21
37
|
try {
|
|
22
|
-
const containerName = "cybermem-mcp";
|
|
38
|
+
const containerName = process.env.MCP_CONTAINER_NAME || "cybermem-mcp-server-1";
|
|
23
39
|
// Check if container exists
|
|
24
40
|
try {
|
|
25
41
|
(0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio: "pipe" });
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uninstall = uninstall;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const execa_1 = __importDefault(require("execa"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
// Helper to handle and suggest fixes for common errors
|
|
14
|
+
function handleExecError(error, context) {
|
|
15
|
+
const stderr = error.stderr || "";
|
|
16
|
+
let suggestion = "";
|
|
17
|
+
if (stderr.includes("Permission denied") || stderr.includes("publickey")) {
|
|
18
|
+
suggestion =
|
|
19
|
+
"\nš” Next Step: Ensure your SSH keys are added to the remote host: `ssh-copy-id user@host`";
|
|
20
|
+
}
|
|
21
|
+
else if (stderr.includes("docker-compose: command not found")) {
|
|
22
|
+
suggestion =
|
|
23
|
+
"\nš” Next Step: Install Docker and ensure docker-compose is in your PATH.";
|
|
24
|
+
}
|
|
25
|
+
console.error(chalk_1.default.red(`\nā ${context} failed:`), error.message);
|
|
26
|
+
if (suggestion)
|
|
27
|
+
console.log(chalk_1.default.yellow(suggestion));
|
|
28
|
+
// process.exit(1); // Don't always exit on uninstall failure so we can try data wipe?
|
|
29
|
+
}
|
|
30
|
+
async function uninstall(options) {
|
|
31
|
+
let target = "local";
|
|
32
|
+
if (options.rpi)
|
|
33
|
+
target = "rpi";
|
|
34
|
+
if (options.vps)
|
|
35
|
+
target = "vps";
|
|
36
|
+
console.log(chalk_1.default.blue(`Uninstalling CyberMem (${target})...`));
|
|
37
|
+
try {
|
|
38
|
+
if (target === "local") {
|
|
39
|
+
const answers = await inquirer_1.default.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: "confirm",
|
|
42
|
+
name: "confirm",
|
|
43
|
+
message: chalk_1.default.red("Are you sure you want to uninstall CyberMem? This will stop all services."),
|
|
44
|
+
default: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "confirm",
|
|
48
|
+
name: "wipeData",
|
|
49
|
+
message: chalk_1.default.yellow("Do you also want to wipe all data (~/.cybermem)?"),
|
|
50
|
+
default: false,
|
|
51
|
+
when: (a) => a.confirm,
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
if (!answers.confirm) {
|
|
55
|
+
console.log(chalk_1.default.gray("Aborted."));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Resolve Template Directory
|
|
59
|
+
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
60
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
61
|
+
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
62
|
+
}
|
|
63
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
64
|
+
templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
|
|
65
|
+
}
|
|
66
|
+
const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
|
|
67
|
+
const homeDir = os_1.default.homedir();
|
|
68
|
+
const configDir = path_1.default.join(homeDir, ".cybermem");
|
|
69
|
+
const envFile = path_1.default.join(configDir, ".env");
|
|
70
|
+
console.log(chalk_1.default.blue("Stopping services..."));
|
|
71
|
+
try {
|
|
72
|
+
await (0, execa_1.default)("docker-compose", [
|
|
73
|
+
"-f",
|
|
74
|
+
composeFile,
|
|
75
|
+
"--env-file",
|
|
76
|
+
envFile,
|
|
77
|
+
"--project-name",
|
|
78
|
+
"cybermem",
|
|
79
|
+
"down",
|
|
80
|
+
], {
|
|
81
|
+
stdio: "inherit",
|
|
82
|
+
env: {
|
|
83
|
+
...process.env,
|
|
84
|
+
DATA_DIR: path_1.default.join(configDir, "data"),
|
|
85
|
+
CYBERMEM_ENV_PATH: envFile,
|
|
86
|
+
OM_API_KEY: "",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
handleExecError(e, "Local uninstall");
|
|
92
|
+
}
|
|
93
|
+
if (answers.wipeData) {
|
|
94
|
+
console.log(chalk_1.default.yellow(`Wiping configuration at ${configDir}...`));
|
|
95
|
+
fs_1.default.rmSync(configDir, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
console.log(chalk_1.default.green("ā
CyberMem uninstalled successfully."));
|
|
98
|
+
}
|
|
99
|
+
else if (target === "rpi" || target === "vps") {
|
|
100
|
+
let sshHost = options.host;
|
|
101
|
+
if (!sshHost) {
|
|
102
|
+
const answers = await inquirer_1.default.prompt([
|
|
103
|
+
{
|
|
104
|
+
type: "input",
|
|
105
|
+
name: "host",
|
|
106
|
+
message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
|
|
107
|
+
validate: (input) => input.includes("@") ? true : "Format must be user@host",
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
sshHost = answers.host;
|
|
111
|
+
}
|
|
112
|
+
console.log(chalk_1.default.blue(`Uninstalling remote host ${sshHost} via Ansible...`));
|
|
113
|
+
// Resolve Ansible Paths
|
|
114
|
+
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
115
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
116
|
+
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
117
|
+
}
|
|
118
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
119
|
+
templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
|
|
120
|
+
}
|
|
121
|
+
const playbookPath = path_1.default.join(templateDir, "ansible/playbooks/deploy-cybermem.yml");
|
|
122
|
+
const ansibleDir = path_1.default.join(templateDir, "ansible");
|
|
123
|
+
const host = sshHost.split("@")[1];
|
|
124
|
+
const sshUser = sshHost.split("@")[0];
|
|
125
|
+
// We use state=absent via extra vars to trigger a teardown in the playbook
|
|
126
|
+
// (Assuming the playbook supports it or we add support)
|
|
127
|
+
// For now, let's use a simple remote command until we harden the playbook to support uninstall
|
|
128
|
+
const remoteCmd = `
|
|
129
|
+
cd ~/.cybermem && docker-compose down
|
|
130
|
+
rm -rf ~/.cybermem/docker-compose.yml
|
|
131
|
+
`;
|
|
132
|
+
try {
|
|
133
|
+
await (0, execa_1.default)("ssh", [sshHost, remoteCmd], { stdio: "inherit" });
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
handleExecError(e, "Remote uninstall");
|
|
137
|
+
}
|
|
138
|
+
console.log(chalk_1.default.green("ā
Remote uninstallation complete!"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error(chalk_1.default.red("Uninstall failed:"), error);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|