@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
package/dist/commands/upgrade.js
CHANGED
|
@@ -10,26 +10,46 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
11
|
const os_1 = __importDefault(require("os"));
|
|
12
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("Connection timed out") ||
|
|
22
|
+
stderr.includes("Could not resolve host")) {
|
|
23
|
+
suggestion =
|
|
24
|
+
"\nš” Next Step: Check your network connection or the remote host's availability.";
|
|
25
|
+
}
|
|
26
|
+
else if (stderr.includes("ansible-playbook: command not found")) {
|
|
27
|
+
suggestion =
|
|
28
|
+
"\nš” Next Step: Install Ansible: `brew install ansible` (on macOS).";
|
|
29
|
+
}
|
|
30
|
+
else if (stderr.includes("docker-compose: command not found")) {
|
|
31
|
+
suggestion =
|
|
32
|
+
"\nš” Next Step: Install Docker and ensure docker-compose is in your PATH.";
|
|
33
|
+
}
|
|
34
|
+
console.error(chalk_1.default.red(`\nā ${context} failed:`), error.message);
|
|
35
|
+
if (suggestion)
|
|
36
|
+
console.log(chalk_1.default.yellow(suggestion));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
13
39
|
async function upgrade(options) {
|
|
14
40
|
let target = "local";
|
|
15
41
|
if (options.rpi)
|
|
16
42
|
target = "rpi";
|
|
17
43
|
if (options.vps)
|
|
18
44
|
target = "vps";
|
|
19
|
-
|
|
45
|
+
const isStaging = !!options.staging;
|
|
46
|
+
const envType = isStaging ? "staging" : "prod";
|
|
47
|
+
const projectSuffix = isStaging ? "-staging" : "";
|
|
48
|
+
console.log(chalk_1.default.blue(`Upgrading CyberMem (${target}-${envType})...`));
|
|
20
49
|
try {
|
|
21
50
|
if (target === "local") {
|
|
22
51
|
console.log(chalk_1.default.blue("Pulling latest Docker images..."));
|
|
23
|
-
//
|
|
24
|
-
// "upgrade" implies updating running instance.
|
|
25
|
-
// running instance uses ~/.cybermem/docker-compose.yml (if init copied it?)
|
|
26
|
-
// Wait, init uses template directly?
|
|
27
|
-
// "deploy/init" logic for local:
|
|
28
|
-
// "docker-compose -f composeFile ...". composeFile was from templateDir.
|
|
29
|
-
// It did NOT copy compose file to ~/.cybermem for local.
|
|
30
|
-
// It uses the installed package's template.
|
|
31
|
-
// So for upgrade (which might be run from newer CLI version), we use the NEW CLI's template.
|
|
32
|
-
// Resolve Template Directory (Same logic as init)
|
|
52
|
+
// Resolve Template Directory
|
|
33
53
|
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
34
54
|
if (!fs_1.default.existsSync(templateDir)) {
|
|
35
55
|
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
@@ -47,25 +67,35 @@ async function upgrade(options) {
|
|
|
47
67
|
const homeDir = os_1.default.homedir();
|
|
48
68
|
const configDir = path_1.default.join(homeDir, ".cybermem");
|
|
49
69
|
const envFile = path_1.default.join(configDir, ".env");
|
|
50
|
-
const dataDir = path_1.default.join(configDir, "data");
|
|
70
|
+
const dataDir = path_1.default.join(configDir, isStaging ? "data-staging" : "data");
|
|
51
71
|
// Pull images
|
|
52
|
-
|
|
53
|
-
"-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
try {
|
|
73
|
+
await (0, execa_1.default)("docker-compose", [
|
|
74
|
+
"-f",
|
|
75
|
+
composeFile,
|
|
76
|
+
"--env-file",
|
|
77
|
+
envFile,
|
|
78
|
+
"--project-name",
|
|
79
|
+
`cybermem${projectSuffix}`,
|
|
80
|
+
"pull",
|
|
81
|
+
], {
|
|
82
|
+
stdio: "inherit",
|
|
83
|
+
env: {
|
|
84
|
+
...process.env,
|
|
85
|
+
DATA_DIR: dataDir,
|
|
86
|
+
CYBERMEM_ENV_PATH: envFile,
|
|
87
|
+
OM_API_KEY: "", // Local bypass
|
|
88
|
+
PROJECT_NAME: `cybermem${projectSuffix}`,
|
|
89
|
+
TRAEFIK_PORT: isStaging ? "8625" : "8626",
|
|
90
|
+
DASHBOARD_PORT: isStaging ? "3001" : "3000",
|
|
91
|
+
CYBERMEM_ENV: envType,
|
|
92
|
+
CYBERMEM_INSTANCE: target,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
handleExecError(e, "Local image pull");
|
|
98
|
+
}
|
|
69
99
|
// Up (recreate)
|
|
70
100
|
console.log(chalk_1.default.blue("Restarting services..."));
|
|
71
101
|
await (0, execa_1.default)("docker-compose", [
|
|
@@ -74,7 +104,7 @@ async function upgrade(options) {
|
|
|
74
104
|
"--env-file",
|
|
75
105
|
envFile,
|
|
76
106
|
"--project-name",
|
|
77
|
-
|
|
107
|
+
`cybermem${projectSuffix}`,
|
|
78
108
|
"up",
|
|
79
109
|
"-d",
|
|
80
110
|
"--remove-orphans",
|
|
@@ -85,12 +115,17 @@ async function upgrade(options) {
|
|
|
85
115
|
DATA_DIR: dataDir,
|
|
86
116
|
CYBERMEM_ENV_PATH: envFile,
|
|
87
117
|
OM_API_KEY: "", // Local bypass
|
|
118
|
+
PROJECT_NAME: `cybermem${projectSuffix}`,
|
|
119
|
+
TRAEFIK_PORT: isStaging ? "8625" : "8626",
|
|
120
|
+
DASHBOARD_PORT: isStaging ? "3001" : "3000",
|
|
121
|
+
CYBERMEM_ENV: envType,
|
|
122
|
+
CYBERMEM_INSTANCE: target,
|
|
88
123
|
},
|
|
89
124
|
});
|
|
90
125
|
console.log(chalk_1.default.green("ā
Upgrade complete!"));
|
|
91
126
|
}
|
|
92
127
|
else if (target === "rpi" || target === "vps") {
|
|
93
|
-
// Remote upgrade
|
|
128
|
+
// Remote upgrade via Ansible
|
|
94
129
|
let sshHost = options.host;
|
|
95
130
|
if (!sshHost) {
|
|
96
131
|
const answers = await inquirer_1.default.prompt([
|
|
@@ -103,9 +138,8 @@ async function upgrade(options) {
|
|
|
103
138
|
]);
|
|
104
139
|
sshHost = answers.host;
|
|
105
140
|
}
|
|
106
|
-
console.log(chalk_1.default.blue(`Upgrading remote host ${sshHost}...`));
|
|
107
|
-
// 1.
|
|
108
|
-
// Resolve Template Directory
|
|
141
|
+
console.log(chalk_1.default.blue(`Upgrading remote host ${sshHost} via Ansible...`));
|
|
142
|
+
// 1. Resolve Template Directory
|
|
109
143
|
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
110
144
|
if (!fs_1.default.existsSync(templateDir)) {
|
|
111
145
|
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
@@ -113,25 +147,30 @@ async function upgrade(options) {
|
|
|
113
147
|
if (!fs_1.default.existsSync(templateDir)) {
|
|
114
148
|
templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
|
|
115
149
|
}
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
const playbookPath = path_1.default.join(templateDir, "ansible/playbooks/deploy-cybermem.yml");
|
|
151
|
+
const ansibleDir = path_1.default.join(templateDir, "ansible");
|
|
152
|
+
const [sshUser, host] = sshHost.split("@");
|
|
153
|
+
// 2. Run Ansible Playbook
|
|
154
|
+
// For upgrade, the playbook's default state (started) will pull latest images
|
|
155
|
+
// if we ensure it performs a pull. Our playbook already pulls images.
|
|
156
|
+
try {
|
|
157
|
+
await (0, execa_1.default)("ansible-playbook", [
|
|
158
|
+
"-i",
|
|
159
|
+
`${host},`,
|
|
160
|
+
"-u",
|
|
161
|
+
sshUser,
|
|
162
|
+
playbookPath,
|
|
163
|
+
"--extra-vars",
|
|
164
|
+
`ansible_ssh_extra_args='-o StrictHostKeyChecking=no' CYBERMEM_ENV=${envType} TRAEFIK_PORT=${isStaging ? "8625" : "8626"} PROJECT_NAME=cybermem${projectSuffix} project_dir=/home/${sshUser}/cybermem${projectSuffix}`,
|
|
165
|
+
], {
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
cwd: ansibleDir,
|
|
168
|
+
});
|
|
118
169
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`${sshHost}:~/.cybermem/docker-compose.yml`,
|
|
124
|
-
]);
|
|
125
|
-
// 2. Pull and Up on Remote
|
|
126
|
-
const remoteCmd = `
|
|
127
|
-
export CYBERMEM_ENV_PATH=~/.cybermem/.env
|
|
128
|
-
export DATA_DIR=~/.cybermem/data
|
|
129
|
-
export DOCKER_DEFAULT_PLATFORM=linux/arm64
|
|
130
|
-
docker-compose -f ~/.cybermem/docker-compose.yml pull
|
|
131
|
-
docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
|
|
132
|
-
`;
|
|
133
|
-
await (0, execa_1.default)("ssh", [sshHost, remoteCmd], { stdio: "inherit" });
|
|
134
|
-
console.log(chalk_1.default.green("ā
Remote upgrade complete!"));
|
|
170
|
+
catch (e) {
|
|
171
|
+
handleExecError(e, "Remote upgrade");
|
|
172
|
+
}
|
|
173
|
+
console.log(chalk_1.default.green("ā
Remote upgrade complete via Ansible!"));
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
176
|
catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -4,23 +4,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const backup_1 = require("./commands/backup");
|
|
6
6
|
const dashboard_1 = require("./commands/dashboard");
|
|
7
|
-
const
|
|
7
|
+
const install_1 = require("./commands/install");
|
|
8
8
|
const reset_1 = require("./commands/reset");
|
|
9
9
|
const restore_1 = require("./commands/restore");
|
|
10
|
+
const uninstall_1 = require("./commands/uninstall");
|
|
10
11
|
const upgrade_1 = require("./commands/upgrade");
|
|
11
12
|
const program = new commander_1.Command();
|
|
12
13
|
program
|
|
13
14
|
.name("mcp")
|
|
14
15
|
.description("CyberMem - Deploy your AI memory server in one command")
|
|
15
16
|
.version("1.0.0");
|
|
16
|
-
// Command:
|
|
17
|
+
// Command: Install
|
|
17
18
|
program
|
|
18
|
-
.command("
|
|
19
|
-
.description("
|
|
19
|
+
.command("install")
|
|
20
|
+
.description("Install CyberMem (Scaffold & Start)")
|
|
20
21
|
.option("--rpi", "Deploy to Raspberry Pi")
|
|
21
22
|
.option("--vps", "Deploy to VPS/Cloud server")
|
|
23
|
+
.option("--staging", "Deploy to staging environment (different ports/data)")
|
|
22
24
|
.option("--remote-access", "Enable Tailscale Funnel for HTTPS remote access")
|
|
23
|
-
.
|
|
25
|
+
.option("--host <host>", "SSH host for remote deploy (e.g. pi@raspberrypi.local)")
|
|
26
|
+
.action(install_1.install);
|
|
27
|
+
// Command: Uninstall
|
|
28
|
+
program
|
|
29
|
+
.command("uninstall")
|
|
30
|
+
.description("Uninstall CyberMem (Teardown services)")
|
|
31
|
+
.option("--rpi", "Uninstall from Raspberry Pi")
|
|
32
|
+
.option("--vps", "Uninstall from VPS/Cloud server")
|
|
33
|
+
.option("--host <host>", "SSH host for remote uninstall")
|
|
34
|
+
.action(uninstall_1.uninstall);
|
|
24
35
|
// Command: Upgrade
|
|
25
36
|
program
|
|
26
37
|
.command("upgrade")
|
|
@@ -28,6 +39,7 @@ program
|
|
|
28
39
|
.option("--local", "Upgrade local instance (default)")
|
|
29
40
|
.option("--rpi", "Upgrade remote RPi")
|
|
30
41
|
.option("--vps", "Upgrade remote VPS")
|
|
42
|
+
.option("--staging", "Upgrade staging environment")
|
|
31
43
|
.option("--host <host>", "SSH host for remote upgrade (e.g. pi@raspberrypi.local)")
|
|
32
44
|
.action(upgrade_1.upgrade);
|
|
33
45
|
program
|
|
@@ -42,6 +54,7 @@ program
|
|
|
42
54
|
program
|
|
43
55
|
.command("reset")
|
|
44
56
|
.description("Reset (wipe) the CyberMem database - DESTRUCTIVE!")
|
|
57
|
+
.option("-f, --force", "Skip confirmation prompt")
|
|
45
58
|
.action(reset_1.reset);
|
|
46
59
|
program
|
|
47
60
|
.command("dashboard")
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
- name: Deploy CyberMem to Raspberry Pi
|
|
3
|
-
hosts:
|
|
4
|
-
become:
|
|
3
|
+
hosts: all
|
|
4
|
+
become: true
|
|
5
5
|
vars:
|
|
6
6
|
project_dir: /home/{{ ansible_user }}/cybermem
|
|
7
7
|
env_file: .env.rpi
|
|
8
|
+
# Default ports based on environment
|
|
9
|
+
TRAEFIK_PORT_STAGING: "8625"
|
|
10
|
+
TRAEFIK_PORT_PROD: "8626"
|
|
11
|
+
TRAEFIK_PORT_DEFAULT: "{{ TRAEFIK_PORT_STAGING if cybermem_env | default('prod') == 'staging' else TRAEFIK_PORT_PROD }}"
|
|
12
|
+
# Port to use consistently across all tasks
|
|
13
|
+
EFFECTIVE_TRAEFIK_PORT: "{{ TRAEFIK_PORT | default(TRAEFIK_PORT_DEFAULT) }}"
|
|
8
14
|
|
|
9
15
|
tasks:
|
|
10
16
|
- name: Update apt cache
|
|
11
17
|
apt:
|
|
12
|
-
update_cache:
|
|
18
|
+
update_cache: true
|
|
13
19
|
cache_valid_time: 3600
|
|
14
20
|
|
|
15
21
|
- name: Install dependencies
|
|
@@ -23,13 +29,13 @@
|
|
|
23
29
|
service:
|
|
24
30
|
name: docker
|
|
25
31
|
state: started
|
|
26
|
-
enabled:
|
|
32
|
+
enabled: true
|
|
27
33
|
|
|
28
34
|
- name: Add user to docker group
|
|
29
35
|
user:
|
|
30
36
|
name: "{{ ansible_user }}"
|
|
31
37
|
groups: docker
|
|
32
|
-
append:
|
|
38
|
+
append: true
|
|
33
39
|
|
|
34
40
|
- name: Create project directory
|
|
35
41
|
file:
|
|
@@ -39,33 +45,204 @@
|
|
|
39
45
|
group: "{{ ansible_user }}"
|
|
40
46
|
mode: "0755"
|
|
41
47
|
|
|
42
|
-
- name:
|
|
48
|
+
- name: Ensure backup directory exists
|
|
49
|
+
file:
|
|
50
|
+
path: "{{ project_dir }}/backups"
|
|
51
|
+
state: directory
|
|
52
|
+
owner: "{{ ansible_user }}"
|
|
53
|
+
group: "{{ ansible_user }}"
|
|
54
|
+
mode: "0755"
|
|
55
|
+
|
|
56
|
+
- name: Ensure secrets directory exists
|
|
57
|
+
file:
|
|
58
|
+
path: "{{ project_dir }}/secrets"
|
|
59
|
+
state: directory
|
|
60
|
+
owner: "{{ ansible_user }}"
|
|
61
|
+
group: "{{ ansible_user }}"
|
|
62
|
+
mode: "0755"
|
|
63
|
+
|
|
64
|
+
- name: Write API Key Secret File
|
|
65
|
+
copy:
|
|
66
|
+
content: "{{ auth_token_value }}"
|
|
67
|
+
dest: "{{ project_dir }}/secrets/om_api_key"
|
|
68
|
+
owner: "{{ ansible_user }}"
|
|
69
|
+
group: "{{ ansible_user }}"
|
|
70
|
+
mode: "0600"
|
|
71
|
+
when: auth_token_value is defined
|
|
72
|
+
ignore_errors: true
|
|
73
|
+
|
|
74
|
+
- name: Ensure data directory exists
|
|
75
|
+
file:
|
|
76
|
+
path: "{{ project_dir }}/data"
|
|
77
|
+
state: directory
|
|
78
|
+
owner: "{{ ansible_user }}"
|
|
79
|
+
group: "{{ ansible_user }}"
|
|
80
|
+
mode: "0755"
|
|
81
|
+
|
|
82
|
+
- name: Check if database exists
|
|
83
|
+
stat:
|
|
84
|
+
path: "{{ project_dir }}/data/openmemory.sqlite"
|
|
85
|
+
register: db_file
|
|
86
|
+
|
|
87
|
+
- name: Create pre-deploy database backup
|
|
88
|
+
shell: "cp {{ project_dir }}/data/openmemory.sqlite {{ project_dir }}/backups/openmemory.sqlite.{{ ansible_facts['date_time']['iso8601_basic_short'] }}.bak"
|
|
89
|
+
when: db_file.stat.exists
|
|
90
|
+
ignore_errors: true
|
|
91
|
+
|
|
92
|
+
- name: Copy Docker Compose template
|
|
93
|
+
copy:
|
|
94
|
+
src: "../../docker-compose.yml"
|
|
95
|
+
dest: "{{ project_dir }}/docker-compose.yml"
|
|
96
|
+
owner: "{{ ansible_user }}"
|
|
97
|
+
group: "{{ ansible_user }}"
|
|
98
|
+
mode: "0644"
|
|
99
|
+
|
|
100
|
+
- name: Replace GHCR image tag (staging uses :staging, prod uses :latest)
|
|
101
|
+
replace:
|
|
102
|
+
path: "{{ project_dir }}/docker-compose.yml"
|
|
103
|
+
regexp: "(ghcr.io/[^/]+/cybermem-[^:]+):latest"
|
|
104
|
+
replace: '\1:{{ ghcr_tag | default("latest") }}'
|
|
105
|
+
when: ghcr_tag is defined and ghcr_tag != 'latest'
|
|
106
|
+
|
|
107
|
+
- name: Sync monitoring configuration
|
|
43
108
|
synchronize:
|
|
44
|
-
src: "
|
|
45
|
-
dest: "{{ project_dir }}"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
109
|
+
src: "../../monitoring"
|
|
110
|
+
dest: "{{ project_dir }}/"
|
|
111
|
+
recursive: true
|
|
112
|
+
delete: true
|
|
113
|
+
|
|
114
|
+
# NOTE: build_from_source removed - all builds happen on GH Actions now
|
|
115
|
+
|
|
116
|
+
- name: Patch Traefik configuration
|
|
117
|
+
replace:
|
|
118
|
+
path: "{{ project_dir }}/monitoring/traefik/traefik.yml"
|
|
119
|
+
regexp: 'address: ":8626"'
|
|
120
|
+
replace: 'address: ":{{ EFFECTIVE_TRAEFIK_PORT }}"'
|
|
121
|
+
|
|
122
|
+
- name: Ensure .env is present (template if missing)
|
|
56
123
|
copy:
|
|
57
|
-
|
|
124
|
+
content: |
|
|
125
|
+
CYBERMEM_INSTANCE=rpi
|
|
126
|
+
CYBERMEM_ENV={{ cybermem_env | default('prod') }}
|
|
127
|
+
PROJECT_NAME={{ PROJECT_NAME | default('cybermem') }}
|
|
128
|
+
TRAEFIK_PORT={{ EFFECTIVE_TRAEFIK_PORT }}
|
|
129
|
+
CYBERMEM_TAILSCALE={{ CYBERMEM_TAILSCALE | default('false') }}
|
|
130
|
+
SECRETS_DIR={{ project_dir }}/secrets
|
|
131
|
+
DATA_DIR={{ project_dir }}/data
|
|
132
|
+
CYBERMEM_ENV_PATH={{ project_dir }}/.env
|
|
133
|
+
CYBERMEM_HOME={{ project_dir }}
|
|
134
|
+
# Add other envs as needed
|
|
58
135
|
dest: "{{ project_dir }}/.env"
|
|
136
|
+
force: true
|
|
59
137
|
owner: "{{ ansible_user }}"
|
|
60
138
|
group: "{{ ansible_user }}"
|
|
61
139
|
mode: "0600"
|
|
62
140
|
|
|
63
|
-
- name:
|
|
64
|
-
|
|
141
|
+
- name: Load global version from package.json
|
|
142
|
+
set_fact:
|
|
143
|
+
global_version: "{{ (lookup('file', playbook_dir + '/../../../../../package.json') | from_json).version }}"
|
|
144
|
+
|
|
145
|
+
- name: Pull images from GHCR
|
|
146
|
+
command: docker-compose -p {{ PROJECT_NAME | default('cybermem') }} pull
|
|
147
|
+
args:
|
|
148
|
+
chdir: "{{ project_dir }}"
|
|
149
|
+
register: pull_out
|
|
150
|
+
changed_when: "'Downloaded newer image' in pull_out.stdout or 'Downloaded newer image' in pull_out.stderr"
|
|
151
|
+
when: not (skip_pull | default(false) | bool)
|
|
152
|
+
|
|
153
|
+
- name: Stop staging containers (prevent port conflicts, NEVER touch prod)
|
|
154
|
+
shell: |
|
|
155
|
+
docker-compose -p cybermem-staging down --remove-orphans 2>/dev/null || true
|
|
156
|
+
args:
|
|
157
|
+
chdir: "{{ project_dir }}"
|
|
158
|
+
changed_when: false
|
|
159
|
+
ignore_errors: true
|
|
160
|
+
when: cybermem_env | default('prod') == 'staging'
|
|
161
|
+
|
|
162
|
+
- name: Start services
|
|
163
|
+
command: docker-compose -p {{ PROJECT_NAME | default('cybermem') }} up -d --remove-orphans
|
|
65
164
|
args:
|
|
66
165
|
chdir: "{{ project_dir }}"
|
|
67
|
-
register:
|
|
166
|
+
register: up_out
|
|
167
|
+
changed_when: "'Creating' in up_out.stdout or 'Recreating' in up_out.stdout or 'Starting' in up_out.stdout or 'Creating' in up_out.stderr or 'Recreating' in up_out.stderr or 'Starting' in up_out.stderr"
|
|
168
|
+
|
|
169
|
+
- name: Install sqlite3 for token injection
|
|
170
|
+
apt:
|
|
171
|
+
name: sqlite3
|
|
172
|
+
state: present
|
|
173
|
+
|
|
174
|
+
- name: Inject Authentication Token
|
|
175
|
+
shell: |
|
|
176
|
+
sqlite3 {{ project_dir }}/data/openmemory.sqlite "CREATE TABLE IF NOT EXISTS access_keys (
|
|
177
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
178
|
+
key_hash TEXT NOT NULL,
|
|
179
|
+
name TEXT DEFAULT 'default',
|
|
180
|
+
user_id TEXT DEFAULT 'default',
|
|
181
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
182
|
+
last_used_at TEXT,
|
|
183
|
+
is_active INTEGER DEFAULT 1
|
|
184
|
+
);"
|
|
185
|
+
sqlite3 {{ project_dir }}/data/openmemory.sqlite "DELETE FROM access_keys WHERE name='{{ auth_token_name | default('ansible-generated') }}';"
|
|
186
|
+
sqlite3 {{ project_dir }}/data/openmemory.sqlite "INSERT INTO access_keys (id, key_hash, name, user_id) VALUES ('{{ auth_token_id }}', '{{ auth_token_hash }}', '{{ auth_token_name | default('ansible-generated') }}', 'admin');"
|
|
187
|
+
when: auth_token_hash is defined
|
|
188
|
+
ignore_errors: true
|
|
189
|
+
|
|
190
|
+
- name: Wait for Traefik to be ready
|
|
191
|
+
uri:
|
|
192
|
+
url: "http://localhost:{{ EFFECTIVE_TRAEFIK_PORT }}/health"
|
|
193
|
+
status_code: 200
|
|
194
|
+
register: traefik_ping
|
|
195
|
+
until: traefik_ping.status == 200
|
|
196
|
+
retries: 10
|
|
197
|
+
delay: 5
|
|
198
|
+
|
|
199
|
+
- name: Verify Overall System Health
|
|
200
|
+
uri:
|
|
201
|
+
url: "http://localhost:{{ EFFECTIVE_TRAEFIK_PORT }}/api/health"
|
|
202
|
+
return_content: true
|
|
203
|
+
register: health_status
|
|
204
|
+
until: "health_status.content is defined and (health_status.content | from_json).overall == 'ok'"
|
|
205
|
+
retries: 5
|
|
206
|
+
delay: 10
|
|
207
|
+
ignore_errors: true
|
|
208
|
+
|
|
209
|
+
- name: Configure Tailscale Funnel (after health check)
|
|
210
|
+
shell: |
|
|
211
|
+
# Reset any existing configuration for clean state
|
|
212
|
+
tailscale serve reset 2>/dev/null || true
|
|
213
|
+
tailscale funnel reset 2>/dev/null || true
|
|
214
|
+
|
|
215
|
+
# Configure port based on current environment only
|
|
216
|
+
if [ "{{ cybermem_env | default('prod') }}" = "staging" ]; then
|
|
217
|
+
tailscale funnel --bg --https=10000 http://127.0.0.1:{{ EFFECTIVE_TRAEFIK_PORT }}
|
|
218
|
+
else
|
|
219
|
+
tailscale funnel --bg --https=443 http://127.0.0.1:{{ EFFECTIVE_TRAEFIK_PORT }}
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
tailscale funnel status
|
|
223
|
+
become: true
|
|
224
|
+
when: (CYBERMEM_TAILSCALE | default('false') | bool) and (health_status is defined and not health_status.failed and (health_status.content | from_json).overall == 'ok')
|
|
225
|
+
register: funnel_status
|
|
226
|
+
ignore_errors: true
|
|
227
|
+
|
|
228
|
+
- name: Display Tailscale Funnel Status
|
|
229
|
+
debug:
|
|
230
|
+
var: funnel_status.stdout_lines
|
|
231
|
+
when: funnel_status is defined and funnel_status.stdout_lines is defined
|
|
232
|
+
|
|
233
|
+
- name: Automated Rollback on Failure
|
|
234
|
+
block:
|
|
235
|
+
- name: Rollback message
|
|
236
|
+
debug:
|
|
237
|
+
msg: "Rollback logic triggered due to unhealthy state."
|
|
238
|
+
|
|
239
|
+
- name: Restart previous state containers
|
|
240
|
+
command: docker-compose -p {{ PROJECT_NAME | default('cybermem') }} up -d --remove-orphans
|
|
241
|
+
args:
|
|
242
|
+
chdir: "{{ project_dir }}"
|
|
243
|
+
ignore_errors: true
|
|
244
|
+
when: health_status.failed or (health_status.content | from_json).overall != 'ok'
|
|
68
245
|
|
|
69
|
-
- name:
|
|
246
|
+
- name: Deployment Summary
|
|
70
247
|
debug:
|
|
71
|
-
|
|
248
|
+
msg: "CyberMem v{{ global_version }} successfully deployed. Health: {{ (health_status.content | from_json).overall | default('UNKNOWN') }}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
- name: Reset CyberMem Database on Raspberry Pi
|
|
3
|
+
hosts: rpi
|
|
4
|
+
become: yes
|
|
5
|
+
vars:
|
|
6
|
+
project_dir: /home/{{ ansible_user }}/cybermem
|
|
7
|
+
db_path: "{{ project_dir }}/data/openmemory.sqlite"
|
|
8
|
+
|
|
9
|
+
tasks:
|
|
10
|
+
- name: Stop CyberMem services
|
|
11
|
+
command: docker-compose down
|
|
12
|
+
args:
|
|
13
|
+
chdir: "{{ project_dir }}"
|
|
14
|
+
|
|
15
|
+
- name: Wipe database file
|
|
16
|
+
file:
|
|
17
|
+
path: "{{ db_path }}"
|
|
18
|
+
state: absent
|
|
19
|
+
|
|
20
|
+
- name: Ensure data directory exists
|
|
21
|
+
file:
|
|
22
|
+
path: "{{ project_dir }}/data"
|
|
23
|
+
state: directory
|
|
24
|
+
owner: "{{ ansible_user }}"
|
|
25
|
+
group: "{{ ansible_user }}"
|
|
26
|
+
mode: "0755"
|
|
27
|
+
|
|
28
|
+
- name: Start CyberMem services
|
|
29
|
+
command: docker-compose up -d
|
|
30
|
+
args:
|
|
31
|
+
chdir: "{{ project_dir }}"
|
|
32
|
+
|
|
33
|
+
- name: Wait for Traefik to be ready
|
|
34
|
+
uri:
|
|
35
|
+
url: "http://localhost:8626/health"
|
|
36
|
+
status_code: 200
|
|
37
|
+
register: traefik_ping
|
|
38
|
+
until: traefik_ping.status == 200
|
|
39
|
+
retries: 10
|
|
40
|
+
delay: 5
|
|
41
|
+
|
|
42
|
+
- name: Database Reset Summary
|
|
43
|
+
debug:
|
|
44
|
+
msg: "CyberMem database at {{ db_path }} has been wiped and services are back online."
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
FROM node:20-alpine AS builder
|
|
1
|
+
FROM node:20-alpine
|
|
3
2
|
WORKDIR /app
|
|
4
|
-
RUN apk add --no-cache
|
|
3
|
+
RUN apk add --no-cache libc6-compat
|
|
5
4
|
COPY package.json ./
|
|
6
5
|
RUN npm install
|
|
7
|
-
|
|
8
|
-
# Production stage
|
|
9
|
-
FROM node:20-alpine AS runner
|
|
10
|
-
WORKDIR /app
|
|
11
|
-
RUN apk add --no-cache libc6-compat
|
|
12
|
-
COPY --from=builder /app/node_modules ./node_modules
|
|
13
6
|
COPY server.js .
|
|
14
|
-
COPY package.json .
|
|
15
7
|
|
|
16
8
|
EXPOSE 3001
|
|
17
9
|
CMD ["node", "server.js"]
|