@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.
Files changed (56) hide show
  1. package/dist/commands/install.js +430 -0
  2. package/dist/commands/reset.js +18 -2
  3. package/dist/commands/uninstall.js +145 -0
  4. package/dist/commands/upgrade.js +91 -52
  5. package/dist/index.js +18 -5
  6. package/dist/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
  7. package/dist/templates/ansible/playbooks/reset-db.yml +44 -0
  8. package/dist/templates/auth-sidecar/Dockerfile +2 -10
  9. package/dist/templates/auth-sidecar/package.json +1 -3
  10. package/dist/templates/auth-sidecar/server.js +149 -110
  11. package/dist/templates/charts/cybermem/.helmignore +13 -0
  12. package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
  13. package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
  14. package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
  15. package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
  16. package/dist/templates/charts/cybermem/templates/secret.yaml +9 -0
  17. package/dist/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
  18. package/dist/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
  19. package/dist/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
  20. package/dist/templates/charts/cybermem/values-vps.yaml +8 -4
  21. package/dist/templates/charts/cybermem/values.yaml +17 -9
  22. package/dist/templates/docker-compose.yml +103 -78
  23. package/dist/templates/monitoring/log_exporter/exporter.py +22 -29
  24. package/dist/templates/monitoring/traefik/traefik.yml +1 -4
  25. package/package.json +9 -3
  26. package/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
  27. package/templates/ansible/playbooks/reset-db.yml +44 -0
  28. package/templates/auth-sidecar/Dockerfile +2 -10
  29. package/templates/auth-sidecar/package.json +1 -3
  30. package/templates/auth-sidecar/server.js +149 -110
  31. package/templates/charts/cybermem/.helmignore +13 -0
  32. package/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
  33. package/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
  34. package/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
  35. package/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
  36. package/templates/charts/cybermem/templates/secret.yaml +9 -0
  37. package/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
  38. package/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
  39. package/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
  40. package/templates/charts/cybermem/values-vps.yaml +8 -4
  41. package/templates/charts/cybermem/values.yaml +17 -9
  42. package/templates/docker-compose.yml +103 -78
  43. package/templates/monitoring/log_exporter/exporter.py +22 -29
  44. package/templates/monitoring/traefik/traefik.yml +1 -4
  45. package/dist/commands/__tests__/backup.test.js +0 -75
  46. package/dist/commands/__tests__/restore.test.js +0 -70
  47. package/dist/commands/deploy.js +0 -239
  48. package/dist/commands/init.js +0 -362
  49. package/dist/commands/login.js +0 -165
  50. package/dist/templates/envs/local.example +0 -27
  51. package/dist/templates/envs/rpi.example +0 -27
  52. package/dist/templates/envs/vps.example +0 -25
  53. package/dist/templates/monitoring/instructions_injector/Dockerfile +0 -15
  54. package/dist/templates/monitoring/instructions_injector/injector.py +0 -137
  55. package/dist/templates/monitoring/instructions_injector/requirements.txt +0 -3
  56. package/dist/templates/openmemory/Dockerfile +0 -19
@@ -6,21 +6,28 @@
6
6
  services:
7
7
  traefik:
8
8
  image: traefik:v3.0
9
- container_name: cybermem-traefik
9
+ # container_name removed for simultaneity
10
10
  command:
11
- - --api.dashboard=true
12
- - --api.insecure=true
13
- - --providers.docker=true
14
- - --providers.docker.exposedbydefault=false
15
- - --entrypoints.web.address=:8626
11
+ - --entryPoints.web.address=:${TRAEFIK_PORT:-8626}
12
+ - --ping=true
13
+ - --ping.entryPoint=web
14
+ - --log.level=INFO
16
15
  - --accesslog=true
17
16
  - --accesslog.filepath=/var/log/traefik/access.log
18
17
  - --accesslog.format=json
18
+ - --accesslog.fields.headers.names.X-Client-Name=keep
19
+ - --accesslog.fields.headers.names.X-Client-Version=keep
20
+ - --providers.docker=true
21
+ - --providers.docker.exposedbydefault=false
22
+ - --providers.docker.constraints=Label(`com.docker.compose.project`, `${PROJECT_NAME:-cybermem}`)
23
+ - --providers.file.directory=/etc/traefik/dynamic
24
+ - --providers.file.watch=true
25
+ - --api.dashboard=true
26
+ - --api.insecure=true
19
27
  ports:
20
- - "8626:8626"
28
+ - "${TRAEFIK_PORT:-8626}:${TRAEFIK_PORT:-8626}"
21
29
  volumes:
22
30
  - /var/run/docker.sock:/var/run/docker.sock:ro
23
- - ./monitoring/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
24
31
  - ./monitoring/traefik/dynamic:/etc/traefik/dynamic:ro
25
32
  - traefik-logs:/var/log/traefik
26
33
  labels:
@@ -35,62 +42,67 @@ services:
35
42
  context: ../../mcp
36
43
  dockerfile: Dockerfile
37
44
  image: ghcr.io/mikhailkogan17/cybermem-mcp:latest
38
- # Note: platform omitted - docker auto-detects (amd64 for CI, arm64 for RPi)
45
+ command:
46
+ [
47
+ "node",
48
+ "dist/index.js",
49
+ "--http",
50
+ "--port",
51
+ "8080",
52
+ "--env",
53
+ "${CYBERMEM_ENV:-prod}",
54
+ ]
39
55
  restart: unless-stopped
40
- container_name: cybermem-mcp
56
+ # container_name removed for simultaneity
41
57
  environment:
42
- OM_TIER: "hybrid"
43
- PORT: "8080"
44
- OM_PORT: "0"
45
58
  OM_DB_PATH: /data/openmemory.sqlite
59
+ CYBERMEM_ENV: ${CYBERMEM_ENV:-prod}
60
+ CYBERMEM_INSTANCE: ${CYBERMEM_INSTANCE:-local}
61
+ CYBERMEM_TAILSCALE: ${CYBERMEM_TAILSCALE:-false}
46
62
  volumes:
47
63
  - ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/.env
48
- - ${HOME}/.cybermem/data:/data
64
+ - ${DATA_DIR:-${HOME}/.cybermem/data}:/data
65
+ - ${SECRETS_DIR:-./secrets}/om_api_key:/run/secrets/om_api_key:ro
49
66
  depends_on:
50
67
  - traefik
51
68
  - auth-sidecar
69
+ # Secrets removed to avoid CI read-only mount issues
70
+ # secrets:
71
+ # - om_api_key
52
72
  labels:
53
73
  - traefik.enable=true
54
- # Middleware definitions (must be on a service that starts BEFORE routes reference them)
55
- # Authenticated routes
56
- - traefik.http.routers.mcp.rule=PathPrefix(`/mcp`) || PathPrefix(`/sse`)
57
- - traefik.http.routers.mcp.entrypoints=web
58
- - traefik.http.routers.mcp.priority=100
59
- - traefik.http.routers.mcp.service=mcp-service
60
- - traefik.http.routers.mcp.middlewares=auth-check
61
- # Public routes (Health/Metrics)
62
- - traefik.http.routers.mcp-public.rule=PathPrefix(`/health`) || PathPrefix(`/metrics`)
63
- - traefik.http.routers.mcp-public.entrypoints=web
64
- - traefik.http.routers.mcp-public.priority=100
65
- - traefik.http.routers.mcp-public.service=mcp-service
66
- - traefik.http.services.mcp-service.loadbalancer.server.port=8080
67
- # Legacy API support (for simple REST clients)
68
- - traefik.http.routers.legacy-api.rule=PathPrefix(`/add`) || PathPrefix(`/query`) || PathPrefix(`/all`)
69
- - traefik.http.routers.legacy-api.entrypoints=web
70
- - traefik.http.routers.legacy-api.priority=100
71
- - traefik.http.routers.legacy-api.service=mcp-service
72
- - traefik.http.routers.legacy-api.middlewares=auth-check
74
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp.rule=PathPrefix(`/mcp`) || PathPrefix(`/sse`)
75
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp.entrypoints=web
76
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp.priority=100
77
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp.service=${PROJECT_NAME:-cybermem}-mcp-service
78
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp.middlewares=auth-check
79
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp-public.rule=PathPrefix(`/health`)
80
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp-public.entrypoints=web
81
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp-public.priority=100
82
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-mcp-public.service=${PROJECT_NAME:-cybermem}-mcp-service
83
+ - traefik.http.services.${PROJECT_NAME:-cybermem}-mcp-service.loadbalancer.server.port=8080
84
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-legacy-api.rule=PathPrefix(`/add`) || PathPrefix(`/query`) || PathPrefix(`/all`) || PathPrefix(`/memory`)
85
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-legacy-api.entrypoints=web
86
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-legacy-api.priority=130
87
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-legacy-api.service=${PROJECT_NAME:-cybermem}-mcp-service
88
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-legacy-api.middlewares=auth-check
73
89
 
74
- # Support legacy /cybermem redirect to root
75
- - traefik.http.middlewares.cybermem-redirect.redirectregex.regex=^https?://[^/]+/cybermem(/.*)?$$
76
- - traefik.http.middlewares.cybermem-redirect.redirectregex.replacement=/$${1}
77
- - traefik.http.middlewares.cybermem-redirect.redirectregex.permanent=false
78
- - traefik.http.routers.cybermem-legacy.rule=PathPrefix(`/cybermem`)
79
- - traefik.http.routers.cybermem-legacy.entrypoints=web
80
- - traefik.http.routers.cybermem-legacy.middlewares=cybermem-redirect
81
- - traefik.http.routers.cybermem-legacy.priority=50
82
-
83
- # Auth sidecar for JWT/API key validation (ForwardAuth)
84
90
  auth-sidecar:
91
+ build:
92
+ context: ./auth-sidecar
93
+ dockerfile: Dockerfile
85
94
  image: ghcr.io/mikhailkogan17/cybermem-auth-sidecar:latest
86
- container_name: cybermem-auth-sidecar
95
+ # container_name removed for simultaneity
87
96
  environment:
88
97
  PORT: "3001"
89
98
  OM_DB_PATH: /data/openmemory.sqlite
90
- API_KEY_FILE: /.env
99
+ CYBERMEM_ENV: ${CYBERMEM_ENV:-prod}
100
+ CYBERMEM_INSTANCE: ${CYBERMEM_INSTANCE:-local}
101
+ CYBERMEM_TAILSCALE: ${CYBERMEM_TAILSCALE:-false}
91
102
  volumes:
92
103
  - ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/.env:ro
93
- - ${HOME}/.cybermem/data:/data
104
+ - ${DATA_DIR:-${HOME}/.cybermem/data}:/data
105
+ - ${SECRETS_DIR:-./secrets}/om_api_key:/run/secrets/om_api_key:ro
94
106
  labels:
95
107
  - traefik.enable=true
96
108
  healthcheck:
@@ -107,27 +119,29 @@ services:
107
119
  timeout: 5s
108
120
  retries: 3
109
121
  restart: unless-stopped
110
-
111
- # Memory engine (MCP + Persistence)
112
- # Uses embedded SQLite stored at ~/.cybermem/data/openmemory.sqlite
122
+ # secrets:
123
+ # - om_api_key
113
124
 
114
125
  db-exporter:
126
+ build:
127
+ context: ./monitoring/db_exporter
128
+ dockerfile: Dockerfile
115
129
  image: ghcr.io/mikhailkogan17/cybermem-db_exporter:latest
116
- container_name: cybermem-db-exporter
130
+ # container_name removed for simultaneity
117
131
  environment:
118
132
  DB_PATH: /data/openmemory.sqlite
119
133
  SCRAPE_INTERVAL: "15"
120
134
  EXPORTER_PORT: "8000"
121
- ports:
122
- - "8000:8000"
123
135
  volumes:
124
- # Mount host openmemory data dir (created by SDK)
125
- - ${HOME}/.cybermem/data:/data
136
+ - ${DATA_DIR:-${HOME}/.cybermem/data}:/data
126
137
  restart: unless-stopped
127
138
 
128
139
  log-exporter:
140
+ build:
141
+ context: ./monitoring/log_exporter
142
+ dockerfile: Dockerfile
129
143
  image: ghcr.io/mikhailkogan17/cybermem-log_exporter:latest
130
- container_name: cybermem-log-exporter
144
+ # container_name removed for simultaneity
131
145
  environment:
132
146
  LOG_FILE: /var/log/traefik/access.log
133
147
  SCRAPE_INTERVAL: "5"
@@ -135,7 +149,7 @@ services:
135
149
  DB_PATH: /data/openmemory.sqlite
136
150
  volumes:
137
151
  - traefik-logs:/var/log/traefik:ro
138
- - ${HOME}/.cybermem/data:/data
152
+ - ${DATA_DIR:-${HOME}/.cybermem/data}:/data
139
153
  - ./monitoring/log_exporter/exporter.py:/app/exporter.py:ro
140
154
  restart: unless-stopped
141
155
  depends_on:
@@ -143,7 +157,7 @@ services:
143
157
 
144
158
  postgres:
145
159
  image: postgres:15-alpine
146
- container_name: cybermem-postgres
160
+ # container_name removed for simultaneity
147
161
  environment:
148
162
  POSTGRES_DB: ${PG_DB:-openmemory}
149
163
  POSTGRES_USER: ${PG_USER:-openmemory}
@@ -163,7 +177,7 @@ services:
163
177
 
164
178
  ollama:
165
179
  image: ollama/ollama:latest
166
- container_name: cybermem-ollama
180
+ # container_name removed for simultaneity
167
181
  ports:
168
182
  - "11434:11434"
169
183
  volumes:
@@ -173,49 +187,60 @@ services:
173
187
  - ollama
174
188
 
175
189
  dashboard:
190
+ build:
191
+ context: ../../dashboard
192
+ dockerfile: Dockerfile
176
193
  image: ghcr.io/mikhailkogan17/cybermem-dashboard:latest
177
- # Note: platform omitted - docker auto-detects (amd64 for CI, arm64 for RPi)
178
- container_name: cybermem-dashboard
194
+ # container_name removed for simultaneity
195
+ # ports:
196
+ # - "${DASHBOARD_PORT:-3000}:3000"
179
197
  environment:
180
198
  DB_EXPORTER_URL: http://db-exporter:8000
181
- # Required for Health Check to find the MCP API internally
182
- CYBERMEM_URL: http://mcp-server:8080
199
+ INTERNAL_MCP_URL: http://mcp-server:8080
183
200
  OM_DB_PATH: /data/openmemory.sqlite
184
- OM_API_KEY: ${OM_API_KEY:-dev-secret-key}
185
- # Tailscale domain for remote config (optional, auto-detected if not set)
186
201
  TAILSCALE_DOMAIN: ${TAILSCALE_DOMAIN:-}
187
- ports:
188
- - "3000:3000"
202
+ CYBERMEM_ENV: ${CYBERMEM_ENV:-prod}
203
+ CYBERMEM_INSTANCE: ${CYBERMEM_INSTANCE:-local}
204
+ CYBERMEM_TAILSCALE: ${CYBERMEM_TAILSCALE:-false}
189
205
  volumes:
190
- - ${HOME}/.cybermem/data:/data
206
+ - ${DATA_DIR:-${HOME}/.cybermem/data}:/data
191
207
  - /var/run/docker.sock:/var/run/docker.sock
192
208
  - ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/app/shared.env
193
209
  labels:
194
210
  - traefik.enable=true
195
- # Dashboard route: root -> dashboard on port 3000
196
- - traefik.http.routers.dashboard.entrypoints=web
197
- - traefik.http.routers.dashboard.rule=PathPrefix(`/`)
198
- - traefik.http.routers.dashboard.priority=1
199
- - traefik.http.routers.dashboard.middlewares=auth-check
200
- - traefik.http.services.dashboard.loadbalancer.server.port=3000
211
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard-public.rule=PathPrefix(`/auth`) || PathPrefix(`/_next`) || PathPrefix(`/static`) || PathPrefix(`/api/auth`)
212
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard-public.entrypoints=web
213
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard-public.priority=120
214
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard-public.service=${PROJECT_NAME:-cybermem}-dashboard
215
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard.rule=PathPrefix(`/`)
216
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard.entrypoints=web
217
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard.priority=1
218
+ - traefik.http.routers.${PROJECT_NAME:-cybermem}-dashboard.middlewares=auth-check
219
+ - traefik.http.services.${PROJECT_NAME:-cybermem}-dashboard.loadbalancer.server.port=3000
201
220
  restart: unless-stopped
221
+ # secrets:
222
+ # - om_api_key
202
223
  depends_on:
203
224
  - db-exporter
204
225
 
205
226
  volumes:
206
227
  openmemory-data:
207
- name: cybermem-data
228
+ name: ${PROJECT_NAME:-cybermem}-data
208
229
  driver: local
209
230
  postgres-data:
210
- name: cybermem-postgres-data
231
+ name: ${PROJECT_NAME:-cybermem}-postgres-data
211
232
  driver: local
212
233
  ollama-models:
213
- name: cybermem-ollama-models
234
+ name: ${PROJECT_NAME:-cybermem}-ollama-models
214
235
  driver: local
215
236
  traefik-logs:
216
- name: cybermem-traefik-logs
237
+ name: ${PROJECT_NAME:-cybermem}-traefik-logs
217
238
  driver: local
218
239
 
240
+ # secrets:
241
+ # om_api_key:
242
+ # file: ${SECRETS_DIR:-./secrets}/om_api_key
243
+
219
244
  networks:
220
245
  default:
221
- name: cybermem-network
246
+ name: ${PROJECT_NAME:-cybermem}-network
@@ -190,20 +190,13 @@ def parse_and_export():
190
190
  status = str(data.get("DownstreamStatus", 0))
191
191
 
192
192
  # Extract MCP client info from custom headers
193
- client_name = data.get("request_X-Client-Name", "unknown")
193
+ client_name = data.get("request_X-Client-Name", "unknown").lower()
194
194
  client_version = data.get("request_X-Client-Version", "unknown")
195
195
 
196
- # Fallback to User-Agent if client_name is unknown
196
+ # Fallback invalid
197
197
  if client_name == "unknown":
198
- ua = data.get("request_User-Agent", "")
199
- if ua and ua != "-":
200
- # Simple heuristic: take the first part before '/' or space
201
- # e.g. "curl/7.64.1" -> "curl", "Mozilla/5.0" -> "Mozilla"
202
- parts = ua.split("/")
203
- if len(parts) > 0:
204
- potential_name = parts[0].split(" ")[0].strip()
205
- if potential_name:
206
- client_name = potential_name
198
+ # STRICT mode: Do NOT fallback to User-Agent
199
+ pass
207
200
 
208
201
  # Remove query params first
209
202
  endpoint = path.split("?")[0]
@@ -234,29 +227,29 @@ def parse_and_export():
234
227
  ):
235
228
  endpoint = "/memory/:id"
236
229
 
237
- # Only track requests to OpenMemory API (/memory/* and /mcp endpoints)
238
230
  # Exclude /health checks - they pollute Top/Last Reader metrics
239
231
  if endpoint.startswith("/memory") or endpoint.startswith("/mcp"):
240
232
  # Check if it's an error (4xx or 5xx)
241
233
  is_error = status.startswith("4") or status.startswith("5")
242
234
 
243
- # Write aggregate stats
244
- increment_stat(client_name, operation, is_error)
245
-
246
- # Log individual request to access_log
247
- log_access(
248
- client_name,
249
- client_version,
250
- method,
251
- endpoint,
252
- operation,
253
- status,
254
- is_error,
255
- )
256
-
257
- print(
258
- f"[{time.strftime('%H:%M:%S')}] {client_name}/{client_version} {method} {endpoint} ({operation}) -> {status}"
259
- )
235
+ # Write aggregate stats AND audit log ONLY for known clients
236
+ if client_name != "unknown" and "health" not in endpoint:
237
+ increment_stat(client_name, operation, is_error)
238
+
239
+ # Log individual request to access_log
240
+ log_access(
241
+ client_name,
242
+ client_version,
243
+ method,
244
+ endpoint,
245
+ operation,
246
+ status,
247
+ is_error,
248
+ )
249
+
250
+ print(
251
+ f"[{time.strftime('%H:%M:%S')}] {client_name}/{client_version} {method} {endpoint} ({operation}) -> {status}"
252
+ )
260
253
 
261
254
  except json.JSONDecodeError:
262
255
  # Skip invalid JSON lines
@@ -26,7 +26,4 @@ providers:
26
26
  file:
27
27
  directory: /etc/traefik/dynamic
28
28
  watch: true
29
-
30
- entryPoints:
31
- web:
32
- address: ":8626"
29
+ # entryPoints and ping moved to docker-compose.yml command for dynamic port support
@@ -1,75 +0,0 @@
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
- const execa_1 = __importDefault(require("execa"));
7
- const fs_1 = __importDefault(require("fs"));
8
- const backup_1 = require("../backup");
9
- // Mock dependencies
10
- jest.mock('execa');
11
- jest.mock('fs');
12
- jest.mock('path', () => ({
13
- ...jest.requireActual('path'),
14
- resolve: jest.fn((...args) => args.join('/')),
15
- basename: jest.fn((p) => p.split('/').pop() || ''),
16
- dirname: jest.fn((p) => p.split('/').slice(0, -1).join('/') || '/'),
17
- }));
18
- // Mock console to keep output clean
19
- const originalConsoleLog = console.log;
20
- const originalConsoleError = console.error;
21
- describe('backup command', () => {
22
- beforeAll(() => {
23
- console.log = jest.fn();
24
- console.error = jest.fn();
25
- });
26
- afterAll(() => {
27
- console.log = originalConsoleLog;
28
- console.error = originalConsoleError;
29
- });
30
- beforeEach(() => {
31
- jest.clearAllMocks();
32
- // Default process.cwd
33
- jest.spyOn(process, 'cwd').mockReturnValue('/mock/cwd');
34
- });
35
- it('should create a backup successfully when openmemory container exists', async () => {
36
- // Mock docker inspect success
37
- execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
38
- // Mock docker run success
39
- execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
40
- // Mock fs.existsSync to true (file created)
41
- fs_1.default.existsSync.mockReturnValue(true);
42
- // Mock fs.statSync
43
- fs_1.default.statSync.mockReturnValue({ size: 5 * 1024 * 1024 }); // 5MB
44
- await (0, backup_1.backup)({});
45
- // Expect docker inspect check
46
- expect(execa_1.default).toHaveBeenCalledWith('docker', ['inspect', 'cybermem-openmemory']);
47
- // Expect docker run tar
48
- const tarCall = execa_1.default.mock.calls[1];
49
- expect(tarCall[0]).toBe('docker');
50
- expect(tarCall[1]).toContain('run');
51
- expect(tarCall[1]).toContain('tar');
52
- expect(tarCall[1]).toContain('czf');
53
- // Check volumes from
54
- expect(tarCall[1]).toContain('--volumes-from');
55
- expect(tarCall[1]).toContain('cybermem-openmemory');
56
- });
57
- it('should fail if openmemory container does not exist', async () => {
58
- // Mock inspect failure
59
- execa_1.default.mockRejectedValueOnce(new Error('No such container'));
60
- const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { }));
61
- await (0, backup_1.backup)({});
62
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
63
- expect(exitSpy).toHaveBeenCalledWith(1);
64
- });
65
- it('should error if backup file is not created', async () => {
66
- execa_1.default.mockResolvedValue({ exitCode: 0 }); // inspect OK
67
- execa_1.default.mockResolvedValue({ exitCode: 0 }); // run OK
68
- // Mock file check false
69
- fs_1.default.existsSync.mockReturnValue(false);
70
- const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { }));
71
- await (0, backup_1.backup)({});
72
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Backup failed'));
73
- expect(exitSpy).toHaveBeenCalledWith(1);
74
- });
75
- });
@@ -1,70 +0,0 @@
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
- const execa_1 = __importDefault(require("execa"));
7
- const fs_1 = __importDefault(require("fs"));
8
- const restore_1 = require("../restore");
9
- jest.mock('execa');
10
- jest.mock('fs');
11
- jest.mock('path', () => ({
12
- ...jest.requireActual('path'),
13
- resolve: jest.fn((...args) => args.join('/')),
14
- basename: jest.fn((p) => p.split('/').pop() || ''),
15
- dirname: jest.fn((p) => p.split('/').slice(0, -1).join('/') || '/'),
16
- }));
17
- const originalConsoleLog = console.log;
18
- const originalConsoleError = console.error;
19
- describe('restore command', () => {
20
- beforeAll(() => {
21
- console.log = jest.fn();
22
- console.error = jest.fn();
23
- });
24
- afterAll(() => {
25
- console.log = originalConsoleLog;
26
- console.error = originalConsoleError;
27
- });
28
- beforeEach(() => {
29
- jest.clearAllMocks();
30
- jest.spyOn(process, 'cwd').mockReturnValue('/mock/cwd');
31
- });
32
- it('should restore successfully', async () => {
33
- // Mock file exists
34
- fs_1.default.existsSync.mockReturnValue(true);
35
- // Mock docker stop (success)
36
- execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
37
- // Mock docker run tar (success)
38
- execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
39
- // Mock docker start (success)
40
- execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
41
- await (0, restore_1.restore)('backup.tar.gz', {});
42
- // Check docker stop
43
- expect(execa_1.default).toHaveBeenCalledWith('docker', ['stop', 'cybermem-openmemory']);
44
- // Check docker run tar
45
- const tarCall = execa_1.default.mock.calls[1];
46
- expect(tarCall[0]).toBe('docker');
47
- expect(tarCall[1]).toContain('run');
48
- expect(tarCall[1]).toContain('tar xzf /backup/backup.tar.gz -C / && chown -R 1001:1001 /data');
49
- // Check docker start
50
- expect(execa_1.default).toHaveBeenCalledWith('docker', ['start', 'cybermem-openmemory']);
51
- });
52
- it('should ignore docker stop error (if container not running)', async () => {
53
- fs_1.default.existsSync.mockReturnValue(true);
54
- // Mock docker stop FAIL
55
- execa_1.default.mockRejectedValueOnce(new Error('No such container'));
56
- // Mock succeeding calls
57
- execa_1.default.mockResolvedValue({ exitCode: 0 });
58
- await (0, restore_1.restore)('backup.tar.gz', {});
59
- // Should still proceed to restore
60
- expect(execa_1.default).toHaveBeenCalledWith('docker', expect.arrayContaining(['run']));
61
- expect(execa_1.default).toHaveBeenCalledWith('docker', ['start', 'cybermem-openmemory']);
62
- });
63
- it('should fail if backup file missing', async () => {
64
- fs_1.default.existsSync.mockReturnValue(false);
65
- const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { }));
66
- await (0, restore_1.restore)('mia.tar.gz', {});
67
- expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
68
- expect(exitSpy).toHaveBeenCalledWith(1);
69
- });
70
- });