@rubytech/create-maxy 1.0.468 → 1.0.470

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.
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================
3
+ # migrate-import.sh — Import a migration bundle into a Maxy install
4
+ #
5
+ # Reads a migration bundle (produced by taskmaster-export.sh) and
6
+ # populates a Maxy/Real Agent installation: Neo4j contacts, memory
7
+ # files, media, conversations, and access PINs.
8
+ #
9
+ # Idempotent: running twice does not create duplicate contacts
10
+ # (MERGE on telephone) or corrupt existing data (file overwrites
11
+ # are safe for migration content).
12
+ #
13
+ # Usage:
14
+ # bash migrate-import.sh <bundle-dir> <install-dir> [--account <uuid>]
15
+ #
16
+ # Arguments:
17
+ # bundle-dir — Migration bundle directory (from taskmaster-export.sh)
18
+ # install-dir — Maxy installation root (contains data/accounts/ and platform/)
19
+ # --account — (Optional) Target account UUID. Required if multiple accounts exist.
20
+ #
21
+ # Example:
22
+ # bash migrate-import.sh ./bundle ~/maxy
23
+ # bash migrate-import.sh ./bundle ~/maxy --account a1b2c3d4-...
24
+ # ============================================================
25
+
26
+ set -euo pipefail
27
+
28
+ # ------------------------------------------------------------------
29
+ # Arguments
30
+ # ------------------------------------------------------------------
31
+ if [ $# -lt 2 ]; then
32
+ echo "[import] ERROR: Usage: migrate-import.sh <bundle-dir> <install-dir> [--account <uuid>]"
33
+ exit 2
34
+ fi
35
+
36
+ BUNDLE_DIR="$1"
37
+ INSTALL_DIR="$2"
38
+ ACCOUNT_FLAG=""
39
+
40
+ shift 2
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --account)
44
+ ACCOUNT_FLAG="$2"
45
+ shift 2
46
+ ;;
47
+ *)
48
+ echo "[import] ERROR: Unknown argument: $1"
49
+ exit 2
50
+ ;;
51
+ esac
52
+ done
53
+
54
+ # Validate bundle
55
+ if [ ! -f "$BUNDLE_DIR/manifest.json" ]; then
56
+ echo "[import] ERROR: manifest.json not found in $BUNDLE_DIR"
57
+ echo "[import] ERROR: Is this a valid migration bundle?"
58
+ exit 1
59
+ fi
60
+
61
+ # Display manifest
62
+ echo "[import] Bundle: $BUNDLE_DIR"
63
+ python3 -c "
64
+ import json
65
+ m = json.load(open('$BUNDLE_DIR/manifest.json'))
66
+ print(f'[import] Source: {m.get(\"source\",\"?\")} v{m.get(\"version\",\"?\")}')
67
+ print(f'[import] Customer: {m.get(\"customer\",\"?\")}')
68
+ print(f'[import] Exported: {m.get(\"exportedAt\",\"?\")}')
69
+ print(f'[import] Hostname: {m.get(\"hostname\",\"?\")}')
70
+ "
71
+
72
+ # ------------------------------------------------------------------
73
+ # Discover account
74
+ # ------------------------------------------------------------------
75
+ ACCOUNTS_DIR="$INSTALL_DIR/data/accounts"
76
+
77
+ if [ ! -d "$ACCOUNTS_DIR" ]; then
78
+ echo "[import] ERROR: No accounts directory at $ACCOUNTS_DIR"
79
+ echo "[import] ERROR: Is $INSTALL_DIR a Maxy installation?"
80
+ exit 1
81
+ fi
82
+
83
+ if [ -n "$ACCOUNT_FLAG" ]; then
84
+ # Explicit account UUID provided
85
+ ACCOUNT_DIR="$ACCOUNTS_DIR/$ACCOUNT_FLAG"
86
+ if [ ! -f "$ACCOUNT_DIR/account.json" ]; then
87
+ echo "[import] ERROR: Account $ACCOUNT_FLAG not found at $ACCOUNT_DIR"
88
+ exit 1
89
+ fi
90
+ ACCOUNT_ID="$ACCOUNT_FLAG"
91
+ else
92
+ # Auto-discover — must be exactly one account
93
+ ACCOUNT_FILES=$(find "$ACCOUNTS_DIR" -maxdepth 2 -name "account.json" 2>/dev/null)
94
+ ACCOUNT_FILE_COUNT=$(echo "$ACCOUNT_FILES" | grep -c '.' || true)
95
+
96
+ if [ "$ACCOUNT_FILE_COUNT" -eq 0 ]; then
97
+ echo "[import] ERROR: No accounts found in $ACCOUNTS_DIR"
98
+ echo "[import] ERROR: Run the Maxy installer first."
99
+ exit 1
100
+ elif [ "$ACCOUNT_FILE_COUNT" -gt 1 ]; then
101
+ echo "[import] ERROR: Multiple accounts found in $ACCOUNTS_DIR:"
102
+ echo "$ACCOUNT_FILES" | while read -r f; do
103
+ echo "[import] $(dirname "$f" | xargs basename)"
104
+ done
105
+ echo "[import] ERROR: Specify which account with --account <uuid>"
106
+ exit 1
107
+ fi
108
+
109
+ ACCOUNT_DIR=$(dirname "$(echo "$ACCOUNT_FILES" | head -1)")
110
+ ACCOUNT_ID=$(basename "$ACCOUNT_DIR")
111
+ fi
112
+
113
+ echo "[import] Target account: $ACCOUNT_ID"
114
+ echo "[import] Account dir: $ACCOUNT_DIR"
115
+
116
+ # ------------------------------------------------------------------
117
+ # Neo4j connection
118
+ # ------------------------------------------------------------------
119
+ NEO4J_URI="${NEO4J_URI:-bolt://localhost:7687}"
120
+ NEO4J_USER="${NEO4J_USER:-neo4j}"
121
+
122
+ NEO4J_PASSWORD_FILE="$INSTALL_DIR/platform/config/.neo4j-password"
123
+ if [ -n "${NEO4J_PASSWORD:-}" ]; then
124
+ : # Explicit env var takes precedence
125
+ elif [ -f "$NEO4J_PASSWORD_FILE" ]; then
126
+ NEO4J_PASSWORD=$(cat "$NEO4J_PASSWORD_FILE")
127
+ else
128
+ echo "[import] ERROR: Neo4j password not found."
129
+ echo "[import] Expected at: $NEO4J_PASSWORD_FILE"
130
+ echo "[import] Or set NEO4J_PASSWORD environment variable."
131
+ exit 1
132
+ fi
133
+
134
+ CYPHER_SHELL="cypher-shell"
135
+ if ! command -v "$CYPHER_SHELL" &> /dev/null; then
136
+ echo "[import] ERROR: cypher-shell not found. Install Neo4j or add cypher-shell to PATH."
137
+ exit 1
138
+ fi
139
+
140
+ # Test connection
141
+ if ! "$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" \
142
+ "RETURN 1" > /dev/null 2>&1; then
143
+ echo "[import] ERROR: Cannot connect to Neo4j at $NEO4J_URI"
144
+ echo "[import] ERROR: Is Neo4j running?"
145
+ exit 1
146
+ fi
147
+ echo "[import] Neo4j connection OK ($NEO4J_URI)"
148
+
149
+ # ------------------------------------------------------------------
150
+ # Helper: escape a string for cypher-shell --param single-quoted value
151
+ # In cypher string literals, single quotes are escaped by doubling: ' → ''
152
+ # ------------------------------------------------------------------
153
+ cypher_escape() {
154
+ echo "$1" | sed "s/'/''/g"
155
+ }
156
+
157
+ # ------------------------------------------------------------------
158
+ # 1. Import contacts
159
+ # ------------------------------------------------------------------
160
+ CONTACTS_DIR="$BUNDLE_DIR/contacts"
161
+ CREATED=0
162
+ SKIPPED=0
163
+ ERRORS=0
164
+
165
+ if [ -d "$CONTACTS_DIR" ]; then
166
+ for CONTACT_FILE in "$CONTACTS_DIR"/*.json; do
167
+ [ -f "$CONTACT_FILE" ] || continue
168
+
169
+ # Extract fields from contact JSON
170
+ GIVEN_NAME=$(python3 -c "import json; d=json.load(open('$CONTACT_FILE')); print(d.get('givenName',''))" 2>/dev/null)
171
+ FAMILY_NAME=$(python3 -c "import json; d=json.load(open('$CONTACT_FILE')); print(d.get('familyName',''))" 2>/dev/null)
172
+ TELEPHONE=$(python3 -c "import json; d=json.load(open('$CONTACT_FILE')); print(d.get('telephone',''))" 2>/dev/null)
173
+ EMAIL=$(python3 -c "import json; d=json.load(open('$CONTACT_FILE')); print(d.get('email',''))" 2>/dev/null)
174
+ JOB_TITLE=$(python3 -c "import json; d=json.load(open('$CONTACT_FILE')); print(d.get('jobTitle',''))" 2>/dev/null)
175
+
176
+ # Validate: must have givenName and at least phone or email
177
+ if [ -z "$GIVEN_NAME" ]; then
178
+ echo "[import] ERROR: contact skipped (no givenName): $(basename "$CONTACT_FILE")"
179
+ ERRORS=$((ERRORS + 1))
180
+ continue
181
+ fi
182
+ if [ -z "$TELEPHONE" ] && [ -z "$EMAIL" ]; then
183
+ echo "[import] ERROR: contact skipped (no phone or email): $GIVEN_NAME"
184
+ ERRORS=$((ERRORS + 1))
185
+ continue
186
+ fi
187
+
188
+ # Build MERGE query with --param for safe parameterisation
189
+ # MERGE on telephone (primary identifier for Taskmaster contacts)
190
+ ESCAPED_GIVEN=$(cypher_escape "$GIVEN_NAME")
191
+ ESCAPED_FAMILY=$(cypher_escape "$FAMILY_NAME")
192
+ ESCAPED_TITLE=$(cypher_escape "$JOB_TITLE")
193
+
194
+ # Build the cypher query — MERGE on telephone for dedup
195
+ CYPHER_QUERY="MERGE (p:Person {telephone: \$phone, accountId: \$acct})"
196
+ CYPHER_QUERY="$CYPHER_QUERY ON CREATE SET p.givenName = \$given, p.source = 'taskmaster-migration', p.status = 'customer', p.createdOn = datetime()"
197
+
198
+ # Add optional fields on CREATE
199
+ if [ -n "$FAMILY_NAME" ]; then
200
+ CYPHER_QUERY="$CYPHER_QUERY, p.familyName = \$family"
201
+ fi
202
+ if [ -n "$EMAIL" ]; then
203
+ CYPHER_QUERY="$CYPHER_QUERY, p.email = \$email"
204
+ fi
205
+ if [ -n "$JOB_TITLE" ]; then
206
+ CYPHER_QUERY="$CYPHER_QUERY, p.jobTitle = \$title"
207
+ fi
208
+
209
+ CYPHER_QUERY="$CYPHER_QUERY RETURN p.givenName AS name, p.telephone AS phone, CASE WHEN p.createdOn = datetime() THEN 'created' ELSE 'exists' END AS status"
210
+
211
+ # Execute with parameterised values
212
+ PARAMS=(
213
+ --param "phone => '$(cypher_escape "$TELEPHONE")'"
214
+ --param "acct => '$(cypher_escape "$ACCOUNT_ID")'"
215
+ --param "given => '$(cypher_escape "$GIVEN_NAME")'"
216
+ )
217
+ [ -n "$FAMILY_NAME" ] && PARAMS+=(--param "family => '$(cypher_escape "$FAMILY_NAME")'")
218
+ [ -n "$EMAIL" ] && PARAMS+=(--param "email => '$(cypher_escape "$(echo "$EMAIL" | tr '[:upper:]' '[:lower:]')")'")
219
+ [ -n "$JOB_TITLE" ] && PARAMS+=(--param "title => '$(cypher_escape "$JOB_TITLE")'")
220
+
221
+ RESULT=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" \
222
+ "${PARAMS[@]}" "$CYPHER_QUERY" 2>&1) || {
223
+ echo "[import] ERROR: contact failed: $GIVEN_NAME <$TELEPHONE> — $RESULT"
224
+ ERRORS=$((ERRORS + 1))
225
+ continue
226
+ }
227
+
228
+ if echo "$RESULT" | grep -q "created"; then
229
+ echo "[import] contact created: $GIVEN_NAME <$TELEPHONE>"
230
+ CREATED=$((CREATED + 1))
231
+ else
232
+ echo "[import] contact skipped (exists): $GIVEN_NAME <$TELEPHONE>"
233
+ SKIPPED=$((SKIPPED + 1))
234
+ fi
235
+ done
236
+ fi
237
+ echo "[import] contacts: $CREATED created, $SKIPPED skipped, $ERRORS errors"
238
+
239
+ # ------------------------------------------------------------------
240
+ # 2. Import memory files
241
+ # ------------------------------------------------------------------
242
+ MEMORY_DIR="$BUNDLE_DIR/memory"
243
+ MEM_COUNT=0
244
+
245
+ if [ -d "$MEMORY_DIR" ]; then
246
+ # Create target memory directories
247
+ mkdir -p "$ACCOUNT_DIR/memory"
248
+
249
+ for SUBDIR in shared admin public; do
250
+ if [ -d "$MEMORY_DIR/$SUBDIR" ] && [ "$(ls -A "$MEMORY_DIR/$SUBDIR" 2>/dev/null)" ]; then
251
+ # Copy recursively, preserving structure
252
+ mkdir -p "$ACCOUNT_DIR/memory/$SUBDIR"
253
+ cp -r "$MEMORY_DIR/$SUBDIR"/* "$ACCOUNT_DIR/memory/$SUBDIR/" 2>/dev/null || true
254
+ COUNT=$(find "$ACCOUNT_DIR/memory/$SUBDIR" -type f 2>/dev/null | wc -l | tr -d ' ')
255
+ echo "[import] memory/$SUBDIR: $COUNT files"
256
+ MEM_COUNT=$((MEM_COUNT + COUNT))
257
+ fi
258
+ done
259
+ fi
260
+ echo "[import] memory total: $MEM_COUNT files"
261
+
262
+ # ------------------------------------------------------------------
263
+ # 3. Import conversations
264
+ # ------------------------------------------------------------------
265
+ CONVOS_DIR="$BUNDLE_DIR/conversations"
266
+ CONVO_COUNT=0
267
+
268
+ if [ -d "$CONVOS_DIR" ]; then
269
+ mkdir -p "$ACCOUNT_DIR/conversations"
270
+
271
+ # Admin conversations
272
+ if [ -d "$CONVOS_DIR/admin" ] && [ "$(ls -A "$CONVOS_DIR/admin" 2>/dev/null)" ]; then
273
+ mkdir -p "$ACCOUNT_DIR/conversations/admin"
274
+ cp "$CONVOS_DIR/admin"/* "$ACCOUNT_DIR/conversations/admin/" 2>/dev/null || true
275
+ COUNT=$(find "$ACCOUNT_DIR/conversations/admin" -type f 2>/dev/null | wc -l | tr -d ' ')
276
+ echo "[import] conversations/admin: $COUNT files"
277
+ CONVO_COUNT=$((CONVO_COUNT + COUNT))
278
+ fi
279
+
280
+ # Per-user conversations
281
+ if [ -d "$CONVOS_DIR/users" ]; then
282
+ for USER_CONVO in "$CONVOS_DIR/users"/*/; do
283
+ [ -d "$USER_CONVO" ] || continue
284
+ USER_KEY=$(basename "$USER_CONVO")
285
+ mkdir -p "$ACCOUNT_DIR/conversations/users/$USER_KEY"
286
+ cp "$USER_CONVO"* "$ACCOUNT_DIR/conversations/users/$USER_KEY/" 2>/dev/null || true
287
+ COUNT=$(find "$ACCOUNT_DIR/conversations/users/$USER_KEY" -type f 2>/dev/null | wc -l | tr -d ' ')
288
+ CONVO_COUNT=$((CONVO_COUNT + COUNT))
289
+ done
290
+ USER_DIRS=$(find "$CONVOS_DIR/users" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
291
+ echo "[import] conversations/users: $USER_DIRS directories"
292
+ fi
293
+
294
+ # Group conversations
295
+ if [ -d "$CONVOS_DIR/groups" ]; then
296
+ for GRP_CONVO in "$CONVOS_DIR/groups"/*/; do
297
+ [ -d "$GRP_CONVO" ] || continue
298
+ GRP_ID=$(basename "$GRP_CONVO")
299
+ mkdir -p "$ACCOUNT_DIR/conversations/groups/$GRP_ID"
300
+ cp -r "$GRP_CONVO"* "$ACCOUNT_DIR/conversations/groups/$GRP_ID/" 2>/dev/null || true
301
+ COUNT=$(find "$ACCOUNT_DIR/conversations/groups/$GRP_ID" -type f 2>/dev/null | wc -l | tr -d ' ')
302
+ CONVO_COUNT=$((CONVO_COUNT + COUNT))
303
+ done
304
+ GRP_DIRS=$(find "$CONVOS_DIR/groups" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
305
+ echo "[import] conversations/groups: $GRP_DIRS directories"
306
+ fi
307
+ fi
308
+ echo "[import] conversations total: $CONVO_COUNT files"
309
+
310
+ # ------------------------------------------------------------------
311
+ # 4. Import media files
312
+ # ------------------------------------------------------------------
313
+ MEDIA_DIR="$BUNDLE_DIR/media"
314
+ MEDIA_COUNT=0
315
+
316
+ if [ -d "$MEDIA_DIR" ]; then
317
+ mkdir -p "$ACCOUNT_DIR/media"
318
+
319
+ for SUBDIR in admin public; do
320
+ if [ -d "$MEDIA_DIR/$SUBDIR" ] && [ "$(ls -A "$MEDIA_DIR/$SUBDIR" 2>/dev/null)" ]; then
321
+ mkdir -p "$ACCOUNT_DIR/media/$SUBDIR"
322
+ cp "$MEDIA_DIR/$SUBDIR"/* "$ACCOUNT_DIR/media/$SUBDIR/" 2>/dev/null || true
323
+ COUNT=$(find "$ACCOUNT_DIR/media/$SUBDIR" -type f 2>/dev/null | wc -l | tr -d ' ')
324
+ echo "[import] media/$SUBDIR: $COUNT files"
325
+ MEDIA_COUNT=$((MEDIA_COUNT + COUNT))
326
+ fi
327
+ done
328
+ fi
329
+ echo "[import] media total: $MEDIA_COUNT files"
330
+
331
+ # ------------------------------------------------------------------
332
+ # 5. Import automations (workflow definitions — reference only)
333
+ # ------------------------------------------------------------------
334
+ AUTO_DIR="$BUNDLE_DIR/automations"
335
+ AUTO_COUNT=0
336
+
337
+ if [ -d "$AUTO_DIR" ] && [ "$(ls -A "$AUTO_DIR" 2>/dev/null)" ]; then
338
+ mkdir -p "$ACCOUNT_DIR/memory/admin/workflows-migrated"
339
+ cp "$AUTO_DIR"/* "$ACCOUNT_DIR/memory/admin/workflows-migrated/" 2>/dev/null || true
340
+ AUTO_COUNT=$(find "$ACCOUNT_DIR/memory/admin/workflows-migrated" -type f 2>/dev/null | wc -l | tr -d ' ')
341
+ echo "[import] automations: $AUTO_COUNT workflow definitions → memory/admin/workflows-migrated/"
342
+ fi
343
+
344
+ # ------------------------------------------------------------------
345
+ # 6. Apply PINs (if present in bundle)
346
+ # ------------------------------------------------------------------
347
+ PINS_FILE="$BUNDLE_DIR/identity/pins.json"
348
+ if [ -f "$PINS_FILE" ]; then
349
+ MASTER_PIN=$(python3 -c "import json; d=json.load(open('$PINS_FILE')); print(d.get('masterPin','') or '')" 2>/dev/null)
350
+ if [ -n "$MASTER_PIN" ]; then
351
+ # Write PIN to account config
352
+ ACCOUNT_JSON="$ACCOUNT_DIR/account.json"
353
+ if [ -f "$ACCOUNT_JSON" ]; then
354
+ python3 -c "
355
+ import json
356
+ acct = json.load(open('$ACCOUNT_JSON'))
357
+ acct['masterPin'] = '$MASTER_PIN'
358
+ json.dump(acct, open('$ACCOUNT_JSON','w'), indent=2)
359
+ print('[import] masterPin applied to account.json')
360
+ "
361
+ else
362
+ echo "[import] WARN: account.json not found — PINs not applied"
363
+ fi
364
+ else
365
+ echo "[import] No masterPin in bundle — skipping PIN application"
366
+ fi
367
+ else
368
+ echo "[import] No pins.json in bundle — skipping PIN application"
369
+ fi
370
+
371
+ # ------------------------------------------------------------------
372
+ # Summary
373
+ # ------------------------------------------------------------------
374
+ echo ""
375
+ echo "[import] =================================================="
376
+ echo "[import] Import complete: $ACCOUNT_ID"
377
+ echo "[import] Contacts: $CREATED created, $SKIPPED skipped, $ERRORS errors"
378
+ echo "[import] Memory files: $MEM_COUNT"
379
+ echo "[import] Conversations: $CONVO_COUNT files"
380
+ echo "[import] Media files: $MEDIA_COUNT"
381
+ echo "[import] Automations: $AUTO_COUNT"
382
+ echo "[import] =================================================="
383
+
384
+ if [ "$ERRORS" -gt 0 ]; then
385
+ echo "[import] WARNING: $ERRORS errors occurred during import — review log above"
386
+ exit 1
387
+ fi